Intents are an essential part of the Android ecosystem. They are used to express an action to be performed and can be classified into implicit and explicit intents. In an abstract way, all intents together define a navigation layer inside an application. In this article we will explain why the Android way to create explicit intents is error-prone and also show some problematic ways to solve it. Finally, we will introduce Dart & Henson, a library that generates this navigation layer and make it easy, convenient, fast and robust to navigate among your activities and services.
Explicit intents are used to run specific components, habitually application internal activities or services. Together with the intent, additional information can be provided to the target components using extras. For instance, the following code creates an explicit intent and triggers an activity:
And this could be the activity being started:
This mechanism is great for component creation and communication, but there are some issues to keep in mind:
- The target component as an entity, does not have any control over its input. In our example, itemId is mandatory, but the “best” DetailActivity can do is to fail at runtime.
- The intent creation is not robust (at all), there is no check on the keys or values attached to the extra bundle.
An ounce of prevention is worth a pound of cure.
A possible solution to those concerns is the Intent Factory pattern. It mainly consists in a factory class which encapsulates methods for every kind of Intent that an application needs. For our example, this might be the Intent Factory:
However, this approach is far from a suitable solution considering that it comes with some constraints:
- The Intent Factory is a large, complex and centralized class.
- It infringes the Meyer’s open/closed principle. It will never get closed, we will always add new methods for each new activity.
- The target component is the one which knows about the parameters, it should be the one containing the logic.
- Optional parameters handling. Should we have a bunch of different methods for the same component? Just one method and send default values?
- It can quickly become tempting for developers to couple methods of the intent factory, one calling others.. and this will become a big ball of mud.
There is a similar strategy which involves distributing these factory methods among the target components. In other words, each component contains one static method (or more) to generate the intent that should be used to start it. It would solve the Meyer’s principle problem, decentralize the intent factories and probably make it easier to avoid the big ball of mud. Nevertheless, the problem about dealing with optional parameters persists. Someone said builder design pattern? Implementing it ourselves?…
I choose a lazy person to do a hard job. Because a lazy person will find an easy way to do it. Bill Gates
Dart 2 & Henson
Dart is an Open Source library for Android. It binds activity fields to intent extras, in the same way Butter Knife does between activity fields and views in XML layouts. Applying it to our example Activity, it evolves into:
The @InjectExtra annotation specifies that a field corresponds to an extra with the same name key. By default, all annotated fields are required and an exception is thrown if the extra is not provided. To remove that check and make it optional, the @Nullable annotation needs to be added. Then, calling Dart.inject is enough to inject the extras using the generated code.
At Groupon, we realized that the information given by those annotations was enough to create the builders that we are looking for. Thus, we decided to bring Dart to higher levels: we introduced an annotation processor that generates the Intent builders for us. This new module is called Henson and is included in Dart 2.
For our DetailActivity, Henson produces a small DSL to navigate to it easily:
The first step is to get the builder for the target activity or service, for that we use Henson.with(context).gotoXXX(). Then, the mandatory extras need to be set using the generated methods named after them. For example, itemId is set using itemId(String str). After that, optional extras can be set using the same approach. And finally, just build and you will get a valid Intent to start your target component!
The DSL is created for all classes which contain @InjectExtra fields. These represent a navigation layer that solves our problem about creating intents:
- The target component has full control over its extras through the annotations.
- The DSL is defined in one place, inside the component. If it is updated, any issue is found at compile time.
- The Meyer’s open/closed principle is not violated. Indeed we have nothing to write, everything is generated for us…
- Optional parameters can be easily provided, since the builder design pattern is used.
- And it comes with auto-completion!
A full working sample can be found here.
Henson creates a small DSL to build intents to your activities and services in a robust way, making it impossible to miss a required extra and flexible enough to add optional arguments as needed. And the best of it, there is no need to write a single line of code, just use Dart 2 and Henson. 😊
Why not to try it?