Android Navigation Made Simple

How many times as an Android developer have you been frustrated with boilerplate code? Does is become tedious for you to create an Intent and then apply all the extras you may need? What about only viewing Activities when a specific state has been handled? Wouldn’t it be nice to prevent navigation to an Activity that first needs some condition to be met?

Well I am, and so I wrote a framework for better Android navigation spawned by an idea from Josh Shepard, a senior developer I worked with.

It’s called Journey and can make app navigation far easier than rewriting numerous lines of repeatable code to handle one of the most trivial aspects of Android development.

To prove to you that it is of any use let’s make a quick Router using the framework so that you can see the benefits of switching your app navigation over to Journey.

example

public class Router {

public static Instance navigateTo;

public static final int REQUEST_VIEW = 0x0002;

public static final String IDS = "ids";

public static final String githubPage = "https://github.com/icarus-sullivan/Journey";

public static void create( Context app ) {
navigateTo = new Journey.Builder( app )
.create( Instance.class );
}
    public interface Instance {

@Extras({"NeedsAuth","Fallback"})
@Route(Activity = MainActivity.class)
void MainActivity( RouteInterceptor interceptor );

@Route(Activity = MainActivity.class)
void MainActivityWithExtras(@Extra(IDS) int[] ids );

@Extras({"NeedsAuth"})
@Route( Activity = WebActivity.class, Url = githubPage )
void GoToGitPage( RouteExtraInterceptor interceptor );

@Route( Action = Intent.ACTION_GET_CONTENT, RequestCode = REQUEST_VIEW )
void GetContent(AppCompatActivity callingAct, Uri uri );

@Route( Action = Intent.ACTION_VIEW )
void ViewInBrowser(Uri uri );

}

}

Okay, here is our Router defined with five dummy routes some corresponding to our app activities and others being handled via Actions. Don’t worry about all the annotation’s yet, we will get into it.

Creating a router helps other developers and yourself define the conditions and values needed by an activity. This prevents a loss of state, data and generally creates an easy template everyone can understand.

Reuse

Let’s assume that the WebActivity referenced in our GoToGitPage route was a generic activity that could handle any incoming url. We could then rewrite our GoToGitPage as follows.

@Route( Activity = WebActivity.class )
void WebActivity( @Extra("URL") String url );

Now when the WebActivity is launched, we can grab the “URL” extra from our intent and display it without having to write the following.

Intent intent = new Intent( this, WebActivity.class );
intent.putExtra("URL", your_url_here);
startActivity( intent );

Instead our route is already defined and your above code can now be called like so.

Router.navigateTo.WebActivity( your_url_here );

This example makes use of two annotations within the framework, @Route and @Extra which corresponds to what they do.

Annotations

@Route can be used to add meta data about the route. Including the following values: Activity, Fragment, Action, Url, and RequestCode.

If you have done any android programming these values should look familiar to you. Here, however, they are consolidated into one point and passed along into the intent appropriately. Now let’s look at @Extra.

@Extra can be used to inject specific key-values into the Intent. Where the value provided in the @Extra(“key”) corresponds to the parameter value provided.

Android intents can only handle specific extras: For a full list of supported extras please view https://github.com/icarus-sullivan/Journey

So let’s run through another scenario where I need to check if a user is authenticated before launching an activity. If they are let’s continue the navigation, otherwise we need to go to a sign in page.

public static final int SIGN_IN_REQUEST = 0x0002;
...
@Route( Activity = UserProfile.class )
void Profile( RouteInterceptor intercept );
@Route( Activity = SignIn.class, RequestCode = SIGN_IN_REQUEST )
void SignIn();

Now when we launch this activity we can check for whether the user is authenticated and redirect if needed.

Router.navigateTo.Profile( new RouteInterceptor() {
@Override
public boolean onRoute(Intent intent) {
if( !User.isAuthenticated() ) {
Router.navigateTo.SignIn(); // redirect
return false;
}

return true; // returning true continues navigation
}
});

Wow, wasn’t that great! However this seems a little tedious to do for every single activity that may need user authentication. So we are going to use a different annotation name @Extras which passes along a string array of values.

Conditional Navigation

@Extras was implemented to declare an array of extras that could decorate methods and allow a non-parameterized way of passing data.

In many different parts of the world selling items to people under a specific age is illegal, so let’s declare two conditions to our navigation method to ensure we don’t wind up in a state we don’t want.

@Extras({"NeedsAuth", "IsAgeAppropriate"})
void PayNow();

If this was a one-off type of interception, then we would probably include a RouteExtraInterceptor in the parameter of PayNow, but since we want this to cascade to all methods decorated with these cases we will add them to our Router as its build.

public class Router {
    public static String NEEDS_AUTH = "NeedsAuth";
public static String AGE_APPROPRIATE = "IsAgeAppropriate";
    public static Instance navigateTo;
    public static void create( Context app ) {
navigateTo = new Journey.Builder( app )
.addInterceptors(new RouteExtraInterceptor() {
@Override
public boolean onRouteExtras(Intent intent, String[] extras) {
for( String extra : extras ) {
if( extra.equals(NEEDS_AUTH) && !User.isAuthenticated() ) {
return false;
}
else if( extra.equals(AGE_APPROPRIATE) && User.getAge() <= 16 ) {
return false;
}
}
return true;
}
})
.create( Instance.class );
}

public interface Instance {
        @Extras({NEEDS_AUTH, AGE_APPROPRIATE})
void PayNow();
    }
}

This interceptor will now be triggered on each navigation to check for auth, and age appropriate usage. Although, we probably don’t need to restrict age for every activity in android — this is an example of how we could.

Additionally, we want this Router to be used anywhere in the app and allow us to navigate freely without worrying about which activity we are in. To build this Router we should extend Application and create it there as follows.

public class App extends Application {

@Override
public void onCreate() {
super.onCreate();

Router.create( getApplicationContext() );
}
}

Now we can use Router.navigateTo.YourRoute() anywhere we want!

What Journey is not.

Journey does not generate or set up AndroidManfiest entries for you, you will still need to add those values. Additionally, any @Route that uses RequestCode must also include a calling activity to send the result back to just as you would in any other case. A quick example below.

@Route( Action = Intent.ACTION_GET_CONTENT, RequestCode = REQUEST_VIEW )
void GetContent(AppCompatActivity callingAct, Uri uri );

Feedback and v2 plans

If you use Journey or would like to contribute please fork me on github and log bugs. https://github.com/icarus-sullivan/Journey

For v2 I would like to clean up some old code, but more importantly look into generating AndroidManfiest entries for each activity.

Thanks for taking the time and making it to the bottom of the post!!!