Router — Everything in its Right Place
Mapping functionalities to app’s components
The library is based on Clay Allsopp’s routable-android project. It helps resolve complex URLs and introduces a few modifications on top:
- Verbose configuration map that allows newbies, non-technical people and veterans to quickly grasp the relationships between different parts of the app
- Configuration map that can be modified at runtime, opening up the possibilities to side-load it from the assets folder, or even change it over the air
- Output setup is delegated to configurable plugins written by the developer. This provides more flexibility in adapting the router to the app’s needs
- Configuration map can be parametrized but it stays logicless
- Optional router-validator gradle plugin is provided to generate Java bindings for appropriate router paths
- Activity/Intent dependencies are dropped
- Router no longer has Activity launching responsibility. Output is simply returned and it’s up to the developer to do something meaningful with it
- Input params and output types can be custom defined if you use AbstractRouter<O, P> — we also provide concrete Router class implementation that takes Bundle as params and returns Bundle as the output
Router takes a URL path as a primary input. Optionally, you can pass param object for later use. Now inside the router, the path is resolved based on the provided configuration map. A route context is created and passed to the appropriate plugins. Inside each plugin we create a fraction of the output. Plugins have access to route context, the resolved part of the map and the output instance. Once all the plugins are done, our output is ready to use.
Map your app
It’s best to start with writing down what your app does. Name each screen and then make a bullet list of the features on each screen. All you need is a good old fashioned piece of paper and pen.
Once you‘re done, you’ll need to transfer your sketch into your app. We found that YAML format works the best for us. It’s very minimalistic and easy to understand. Here’s an example:
Zero level indentations represent paths, e.g. home and user/:id/photos. First level indentations e.g. type, request, decorators are consumed by router’s registered plugins.
Here is a basic boilerplate code to load our app’s specification into an actual router instance:
Plugins are everything
So what do you need all those plugins for? Well, the router itself has ZERO knowledge of anything it does. It only assembles the output given the input plus does some basic operations while resolving URLs. Apps differ from one another therefore it’s the developer’s responsibility to organize the plugins the way it makes sense.
Now let’s write an example plugin for configuring our beloved decorators.
Looking at outputFor method, the config object is what’s provided with the decorators node of the currently resolved URL (see the YAML file). You can do with it whatever you want. Here we just resolve it to decorator classes for later use and, if necessary, put some extra values into a bundle.
With great power comes great responsibility. — Uncle Ben
Bear in mind that these plugins are simply very specific parsers that assemble your output based on the given input. You can do both good and bad with it. The only limitation is your own creativity.
Parameters over Conditions
Before we moved to the router we used to resolve our screens with a bunch of Java methods. Those methods would contain switch cases, if statements and helper methods for evaluating even more conditions. The functionality of a single screen was hidden behind the sloppy code, making everything unmaintainable and unreadable.
With a new router we wanted to get rid of those pitfalls. Screen definitions should be easy to read and write, find and change — easy enough for a designer, product manager, or anyone. No more arbitrary conditions. No more code. Let’s make everything a bullet list!
Having that in mind we replaced conditions with params. If that’s not enough, we can always pass those params to lower layers and deal with them individually. As a result, screen descriptions are logicless and understandable to mortals.
Here are types of router parametrization you can perform:
- path params (a.k.a. local params), e.g. user/:id/photos?key=value
- global params
- extra param
- node params
This part is optional, limited use and supports only specific YAML format with a focus on decorators. Use at your own risk, contributors welcome!
We wrote a gradle plugin called router-validator which is attached to this project. What it does is a very basic parse of your YAML file during compilation time. It makes sure Java code references are properly spelled by generating RouterConstants class — it contains all the used references e.g. static final String RouterConstants.PATH_HOME. Moreover, you can invoke configFor static method “interface” on decorators without any reflection.
Why should I care about this router?
- Single responsibility principle
Router paths describe and define navigation points of the app. Path nodes describe what these paths are made of.
- High Level Organization + Easy Learning Curve
Single and simple configuration file allows newcomers and veterans to quickly grasp an understanding of relations between parts of the app.
- Dynamic yet Strict
The freedom of writing custom plugins is moderated by parametrized & logicless approach to configuration parsing. If you feel like you need an ‘if’ clause somewhere, you are probably designing something wrong.
- Complex URL handling
A bonus you get for free. If your company has a website, you can now easily route people to the equivalent parts of the Android app. If your company is sending push notifications, you can now send a path as a call to action and never code any push notification logic in the app again.
- Painless migration
Let’s say you want to migrate from volley requests to retrofit. Since your requests are defined in a YAML file anyway, all you need is to change the underlying router plugin & network layer implementation. Actually, nothing stops you from migrating straight to iOS since YAML contains no java code.
- A/B Testing + Live Override
Once you have established your router configuration map, you can have many permutations and sideload them from the app’s asset folder or over the air. You can sideload parts of configuration, e.g. change mapping of a single path. You can do all of this while app is running.
Check out the sample NanoRouter app. See how we used AbstractRouter<O, P> with NanoHTTPd and turned Android into a web server.
Last but not least, the default page served by that web server is a more detailed article on using AbstractRouter. Plus, it contains extra optimization tricks on reducing router loading times. Suffice to say we make it 10 times faster — if you’re a curious creature be sure to check git out.