Better Android Intents with Dart & Henson

 

New Features in Realm Java

We recently released version 3.4 of Realm Java today, and with it introduced reverse relationship queries and sync progress listeners. Read on for all the details!

Introduction

I’m Daniel, an Android Developer at Groupon. We have a cool app, using state-of-the-art technology. Everything is optimized, with real life performance, security, and open source; if you want to work on open source, contribute to libraries, or create new ones, please talk to me or visit jobs.groupon.com/careers and see the positions we have for Android developers.

In this talk, I will review intents and extras. I will do a quick introduction on how they create a navigation layer. Then I will show the first model of this library, which is Dart, that is used to consume intents. I will show you how you can use annotations, how to bind the extras, and then how to set it up.

After that, I will discuss Henson, the counterpart used to create intents. I will show some code for intent builders, the fluent API that is used to create intents using those intent builders, then some extras annotations to set up how it works, and finally, how to configure the library.

Intents & Extras

If you go to Android documentation and look for an intent, you will see that it is simply an operation that you want to do, and you can specify.

There are two types of intents. The first one is implicit intents, where you just describe an action. Let’s say we want to send a message; we simply say, “I want to send a message,” and then Android will perform some intent resolution. For instance, let’s say you want to send a message so the user will get a dialog prompt, e.g. you can use WhatsApp, LINE, Facebook Messenger.

You also have explicit intents (the ones that we’ll focus on today): you don’t specify an action. Instead, you specify the component that will be used. Normally, it is in applications; it can be also services, but I will focus on activities.

I will use the Groupon domain, so let’s say we have a list with all the deals. When a user clicks on a deal, we go to deal details, where we have all the details of that deal. To go from one to the other, we use an explicit intent, where we specify the deal details activity.

Sometimes, you need to specify some extra information (in our case, this might be the deal ID). The user taps on a deal, we open the deal details activity, and we say, “I want to see the deal with ID X.”

All intents together create a navigation layer that is used to navigate across our app. It is important to have a good structure, which can make things easier and help us avoid errors.

How do we organize this navigation layer? With big problems, it is better to divide and conquer. I will show you two sides of the same problem and how you can use Dart to solve both of them.

First you need to create an intent. In our case, you have the deal list; when someone taps on an element of the list, we want to create an intent to go to deal details. Then, when we are on the second activity, we want to be able to receive the information (in our case, the extras), and map them.

Dart: Consuming Intents

To consume intents, we use Dart. Let’s think that we have an activity which contains three fields: field1 (a String), field2 (an integer), and field3 (a boolean). This activity is triggered using an intent. That intent might contain some extras.

For example, we have three extras that we want to map: we want to map “value 1” to field1; “value 2” to field2; and “value 3” to field3. The extras are stored in a bundle, that is a kind of map, so we have key values.

Let’s see how we can implement this consumption of intents using just the default in Android (maybe we don’t need any library). Let’s have a look at the code.

public class DealDetailActivity extends Activity {
  public static final String EXTRA_DEAL_ID = "EXTRA_DEAL_ID";
  public static final String EXTRA_SHOW_MAP = "EXTRA_SHOW_MAP";

private String dealId;
private boolean shouldShowMap;

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  dealId = getIntent().getStringExtra(EXTRA_DEAL_ID);
  shouldShowMap = getIntent().getBooleanExtra(EXTRA_SHOW_MAP, false);

  if (dealId == null) {
    throw new IllegalArgumentException("Deal Id is required");
  }
  ... 
}

This is the deal detail activity. We have two inputs: 1) the dealId, which is in a string and is mandatory, so it has to be provided; and 2) shouldShowMap, which is a boolean, and that one is optional. We have defined them.

First we need to get them from intents. We get intent, we get extras. In the second case, shouldShowMap, I see this is optional, we are providing a default value, so if it is not provided, we just use false.

Then, the best that activity can do is to crash, because the activity does not have any control over the input that received. The only thing that can do is, once it is opened, it can check what came from the extras. Here if the dealId was not provided, we just throw an exception.

It is quite a simple activity: we just have two extras, but we have a lot of code. The more code you have, the more issues, the more errors. We don’t want to always repeat a lot of boilerplate. Don’t worry, you can use Dart.

Dart: Annotations & Bindings

Dart is easy to use. First, you need to declare which are your extras, which are the fields that will be mapped to the extras. For Dart, we use the annotation InjectExtra.

public class DealDetailActivity extends Activity { 
  @InjectExtra String dealId;
  @InjectExtra @Nullable Boolean shouldShowMap;
  
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Dart.inject(this);
    ... 
  }
}

Here we have two extras, dealId and shouldShowMap. There’s another annotation that is not coming with a library (it’s from Android): Nullable, which means that the field is optional. Here, we are just defining two extras and saying that the second one is optional.

The second step is to have all the code to make the bindings, to assign the extras to the fields we have. For that, we call Dart.inject. With these two steps, we are mapping the extras we have to the fields we want and haven’t annotated before. If dealId is not provided, we crash the same way we did before.

However, sometimes it is not that easy to do. We might be using a model. Then the fields that we want to map to the extras might not be inside your activity, they might be inside a model class. In this case, we have the field 1, 2, and 3 inside a model that is used by the activity, so you need to do some extra stuff (it’s just one line).

Inside your model, you just annotate the fields.

public class DealDetailModel { 
  @InjectExtra String dealId;
  @InjectExtra @Nullable Boolean shouldShowMap;
}

dealId and shouldShowMap, this is the same, but in your activity, instead of calling Dart.inject, you first specify your model, so Dart will look for the fields there, and then you say “this.”

public class DealDetailActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Dart.inject(detailModelInstance, this);
    ... 
  }

You are saying: “I want to get the extras from this activity.” That’s all. You don’t have to write any code; all the code is generated at compile time.

Dart: Set Up

To set it up, I hope you use Gradle:

compile ‘com.f2prateek.dart:dart:2.0.1
provided ‘com.f2prateek.dart:dart-processor:2.0.1

The first one is a compile dependency, containing all the different type dependencies you will need, e.g. to call Dart.inject. The other one is provided is the processor that is used at compile time to generate all the code that will be used at runtime.

(Check the slides if you still use Maven).

Henson: Creating Intents

The counterpart is how to create intents. We will be using Henson. Henson came with Dart 2.0. In the first version of Dart, we only had the Dart part for consuming intents. However, we realized that still there were some issues.

The activity cannot control what it’s receiving. It can only crash when it is opened. It is like you have a method, or a constructor, and you cannot control which are your parameters (that’s bad!). That’s why at Groupon, we decided to collaborate with the Dart project, and we created Henson.

First, I want to show how I usually create intents, and to let you know why we think that they are wrong.

public class CentralIntentFactory {
  public Intent newDealDetailActivityIntent(Context context,
                                        String dealId) {  
  return new Intent(context, DealDetailActivity.class)
             .putExtra(EXTRA_DEAL_ID, dealId);   
  } 

  public Intent newDealDetailActivityIntent(Context context,   
                                        String dealId,  
                                        boolean showMap) {  
  return new Intent(context, DealDetailActivity.class) 
             .putExtra(EXTRA_DEAL_ID, dealId)  
             .putExtra(EXTRA_SHOW_MAP, showMap); 
  } 
  ...  
}

The first approach is to use a CentralIntentFactory, which is a huge class where you have multiple methods to create the intents to navigate throughout your app. Let’s imagine you have five activities. You have deal details, settings, purchase, a confirmation screen. For each of them, you might have different parameters - you have three methods for each, so you have 15 methods that are calling each other. It is a mess.

First, it is not following Meyer’s Open/Closed Principle, so the class is always open. The class first has many responsibilities, and you will always be modifying the class. When you modify one of your activities, you will be modifying this class. When you add a new activity, you will be modifying this class. So this class will be huge and always open, which is not a good idea.

Also, here we have just one activity, but we have two similar methods. Probably one method will be calling another method, this method another; so in the end you have a big ball of mud there inside, methods calling each other. You will have so many issues and the class will be a nightmare. Better to avoid this approach.

You might be thinking, “That’s fine. I am not using that approach. I don’t have this huge, monster class. What I have is some factory methods inside my activities.”

public class DealDetailActivity extends Activity {
  ...
  public static Intent getIntent(Context context, 
                                 String dealId) {
                                
    return new Intent(context, DealDetailActivity.class)
               .putExtra(EXTRA_DEAL_ID, dealId);
  }
  
  public static Intent getIntent(Context context, 
                                 String dealId,
                                 boolean showMap) {
                                 
    return new Intent(context, DealDetailActivity.class)
               .putExtra(EXTRA_DEAL_ID, itemId)
               .putExtra(EXTRA_SHOW_MAP, showMap);
  }
  ...
}

Here we took the code to generate the intents from this big class and moved it into each activity. Inside the deal detail activity, we have two static methods getIntent, where we provide exactly the same code as before.

Again, you don’t have this huge class, you are not violating the Open/Closed Principle, but still you are writing a lot of code which is boilerplate. Those methods might be calling each other for an activity that is as simple as this one.

It’s not as bad, but when you have four or five parameters for app optional, and two are mandatory, you will have a mess inside a class. We should be able to improve that. Ideally, we shouldn’t be writing a lot of code.

Henson: Intent Builders

If you look at the information we already have by using Dart, we were defining with InjectExtra the fields that we want to map to the extras. With Nullable, we know if a field is mandatory or not.

If you think about it, we already have the information to generate that code. We don’t need to write any code. With Henson, you can create intents without writing any code.

The same way that Dart was generating some code to consume the intents, here we are generating some code as well, but to create intents (those classes are called intent builders).

public class DealDetailActivity$$IntentBuilder {  
  private Intent intent; 
  private Bundler bundler = Bundler.create(); 

  public DealDetailActivity$$IntentBuilder(Context context) {  
    intent = new Intent(context, DealDetailActivity.class);
  } 
 
  public DealDetailActivity$$IntentBuilder dealId(String dealId) { 
    bundler.put("dealId", dealId); 
    return this;  
  } 
   
  public DealDetailActivity$$IntentBuilder shouldShowMap(Boolean shouldShowMap) { 
    bundler.put(shouldShowMap", shouldShowMap); 
    return this;  
  } 
 
  public Intent build() { 
    intent.putExtras(bundler.get()); 
    return intent;  
  }
}

In our case, we have the DealDetailActivity we generate at compile time, so it is not slow for your app. At compile time we generate this code, we are not using any reflection.

You can see the class is simple. You just have the constructor where we provide the context. We create intent, and then with different methods, we can add the different input, and then you have a build method.

This is not a real intent builder; it’s more complex. If one parameter is optional, it changes the order - you first need to provide the mandatory ones, and then the optionals go to the end.

Henson: Fluent API

You don’t need to call intent builders yourself. For Dart, you can use the Henson class.

Intent intent = Henson.with(context)
                .gotoDealDetailActivity()
                .dealId(dealId)
                .shouldShowMap(true)
                .build();

The Henson class provides a fluent API to access intent builders. Doing Henson.with, you provide the context. When you are playing an activity, you should be using the activity context. If you use the application context, it will be using a newer stack, and usually we don’t want that. Instead, you provide the activity context, and then you say goto plus the name of the activity.

This method is generated inside the Henson class. As long as you have the annotation InjectExtra, we will generate all this code for you. Then you need to provide the mandatory parameters, the ones that are required (in our case, dealId), and then you can call build, or provide any optional parameter. You don’t need to do anything. You don’t need to write any code. With this, you can just generate your intents.

Henson: Annotations

There are some exceptions. With the activities, with intents, we are generating a navigation layer. We might want to use this navigation layer to navigate to all our activities. Even if they don’t have any extras. Remember, we are generating code based on the InjectExtra notation. If we have an activity that does not have any extras, we will not generate any code.

For that, we have the annotation HensonNavigable - you simply specify to Henson, “when you are compiling, I want you to generate methods to be able to navigate to this activity.” That way, you can simply call Henson.with(context), go to settings activity, and build.

When we use Dart, this model class is used to map the fields. If you do the same with Henson, you need to specify the class that will be used to generate this fluent API.

@HensonNavigable
public class SettingsActivity extends Activity { 
  
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
  }
  
}

For that, we also use the HensonNavigable annotation. If the deal detail activity does not contain any extras, no code will be generated, we use the HensonNavigable annotation, and then we provide a class that is a model. That way, when we are generating this fluent API, we know which is the class that should be used to generate all the methods.

Model:

public class DealDetailModel { 
  @InjectExtra String dealId;
  @InjectExtra @Nullable Boolean shouldShowMap;
}

Activity:

@HensonNavigable(model = DealDetailModel.class)
public class DealDetailActivity extends Activity {  
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Dart.inject(detailModelInstance, this);
    ... 
}

It is simple to set up for Gradle. You have two dependencies:

compile ‘com.f2prateek.dart:henson:2.0.1
provided ‘com.f2prateek.dart:henson-processor:2.0.1

The first one is compile dependency that contains the Henson class to use at runtime. The second one is the processor that is used at compile time to generate code, with your intent builders and so on.

If you’re using Maven, probably you should move to Gradle (see the slides for additional information).

Code Demo

Here we have many elements (see video). Before, I was using just InjectExtra. I wasn’t providing any extra, any parameter to annotation. If you don’t provide any parameter, Dart will try to find inside your extras one value that has the same key as your parameter. It will look in the bundle for one extra which has this string extra as the key. If you provide something here like this, it will then be a key that will be used to map from the bundle to your fields.

As you can see here, we have a bunch of extras. We specify all of them here. You also can specify if they are optional or not. But, if you are using a boolean, it may not work probably because Nullable doesn’t make any sense when you are using native variables (boolean or integer), that’s fine. It will work anyway. If you want to not have that warning, you can simply use the object.

Also, here we are also using Butterknife. When Dart was developed, the idea was to have a library which is similar to Butterknife, but instead of binding your views, we are binding the extras. It changed the method name, that now it is bind, Butterknife .bind, but that we have kept the same name, .inject. At this time, when you are calling .inject after here, all the extras will be available.

Here is the intent builder that is generated. When you call .inject, we are calling some code that is generated at compile time. This is the intent builder. You can see that it is simple. We have just one method for each extra you have. We are doing nothing strange at compile time. We are just generating the same code that you would need to write, so you don’t need to do it yourself.

I want to show you how to use Henson in some special cases. You see here how we are opening these by creating intent for the simple activity that we just saw. With Henson.with, we provide activity context. Then, we have this method that is automatically generated, because we have this InjectExtra here. We generate this method inside the Henson class. This method is called intent builder, and these are creating it.

Then, you just provide the parameters that are needed. First, you need to provide the mandatory ones. They are in alphabetical order (default key, extra in, etc.), and then you can just provide the options. Finally, you call build. With this, just have intent that you can use to start your activity.

Also, you can use Henson, to navigate two fragments. You can also use Dart to inject extras in your services, or also to attach the arguments in your fragments. With activities that you can call .inject, if it is an activity, will get the extras from the activity. If it is a fragment, it will get the arguments, and if it is a service, it will get the extras as well.

Thank you!

Q & A

Q: Over the last five years, the annotation processors that generate code have become very popular. It’s very common to have five of them running in your project and writing a lot of code at compile time. Do you see any problem with compile times for code generation?

Daniel: Yes, at Groupon we use many libraries, using annotation processing at compile time. We have Dart & Henson; we also have Toothpick, that I would recommend. It is an alternative to Dagger, for dependence injection. This one is completely developed inside Groupon. It is a new approach.

We also use many other libraries, and we haven’t had an issue. Maybe each library will take one, two seconds extra at compile time, but I don’t think that’s a real issue. The gains you have are high. You are avoiding writing out a lot of code, which means less effort. Your app will have a higher quality. You now can control it. That activity will receive the right extras, the right information. Before, you couldn’t control that.

I think that’s fine, to spend two extra seconds at compile time, as long as you can improve the quality of your app.

Q: When you’re building an intent with Henson, your builder methods are called extra int, extra parcelable, extra parcel, parcelable. That’s because the keys were called exactly that, right? That’s not like a generic name. In a real world example, you would say extra, or you could say like campaign ID, campaign table, campaign URL.

Daniel: Yes, exactly. I forgot to explain that. The methods are named after your extras.

If you go here to simple activity, you have a string extra, so you will have a method that is called a string extra. We have one that is called extra string, so we have the method that is called extra string. Here, exactly, yes, they are called after the theme.

Another point that, now that you have been talking about parcelables and so on, there is a library that is called Parceler, which is also using annotation processing, so you don’t have to define all the methods for your classes to make them parcelable. Henson has an integration with this library, so if you are using Parceler, and you provide a parcel using Henson, automatically, we will use that library internally.

Q: If you use Dart Henson, you have no need for the fragment arcs project anymore, right?

Daniel: Yes. You can; there are some libraries. But I would say that with that library you have many functionalities, but with Henson, the point was to have something sharper. We don’t want to have one thing doing many things, we prefer to have small things doing one thing each in a good manner. If you are using that library just for this, you don’t need to use it anymore.

Q: I feel Henson is good idea, and on the other hand, sometimes, the combination of a parameter is very important. First, get intent can receive context and dealId. On the other hand, or secondly, is dealId and ShowMap. So the combination of a parameter is very important information. But I felt that using Henson all parameter can receive. And, so I can imagine, I cannot get the combination information.

Daniel: Yes, that’s right. In the end, the code Henson generates is based on whether an extra is mandatory or not, so we don’t take into account different combinations. As you said, maybe in one method you just want to provide dealId, and another dealId, show ShowMap, and some other things. You don’t have control over that with Henson.

Henson is useful, but maybe not for all your use cases. Maybe when you want to define the parameters that are provided, you can just have one wrapper, or you can connect some methods, and inside those methods, you can use Henson. You don’t need to provide these extras. Still, you are creating more code. You have an intermediate class that you will need to use, but you are sure that this class will be providing the right parameters.

If in your class - in deal detail, let’s say - you should move an extra, automatically, at compile time, your code will fail because that method does not exist anymore. You don’t have control about which are the parameters this provider provided. Henson only checks if the mandatory ones are there.


Daniel Molinero Reguera

Daniel Molinero Reguera

Android Software Engineer at Groupon. Developer with 7 years’ experience building Web and Mobile applications. Solid understanding of software design optimized for embedded systems and the full mobile development lifecycle with the Android SDK. Deep knowledge of data structures, algorithms and security. BSc and MSc in Computer Science with honors. Passionate about innovation and building world class products.

Transcribed by Sandra Sanchez-Roige
Edited by Curtis Chen