Droidkaigi daniel molinero header

Better Android Intents with Dart & Henson

Introduction

My name is Daniel, and an Android Developer at Groupon. We have a cool app, using state-of-the-art technology. 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.

I will review Intents and Extras, 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 also discuss Henson, the counterpart used to create intents.

Intents & Extras

If you go to Android documentation and look for an Intent, you will see it defined as 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 describe an action. Suppose we want to send a message; we simply say, “I want to send a message,” and then Android will perform some intent resolution.

With explicit Intents, you specify the component that will be used. Normally, it is in applications.

I will use the Groupon domain, so let’s say we have a list of 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 with the deal with ID X.

All intents together create a navigation layer that is used to navigate across our app.

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.

Get more development news like this

Suppose we have an Activity that contains three fields:

  • field1 (a String)
  • field2 (an integer)
  • field3 (a boolean)

This activity is triggered using an Intent which may contain 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.

The following is how we would implement this consumption of intents in Android.


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 a string and is mandatory, so it has to be provided; and 2) shouldShowMap, which is a boolean, and that one is optional.

It is quite a simple activity: we have two extras, but we have a lot of code, which can lead to more issues and errors.

Dart: Annotations & Bindings

Dart is easy to use. First, you need to declare your extras. 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);
    ... 
  }
}

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

In the instance in which the fields are in a model class, you can annotate the fields:


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

Instead of calling Dart.inject, you first specify your model, and Dart will look for the fields there:


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

You will not have to write any code, as it is generated at compile time.

Dart: Set Up

To set it up, 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

Henson came with Dart 2.0. Dart originally was able to consume intents.

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 large class with multiple methods to create the intents to navigate throughout your app.

There are issues with this approach. Suppose you have five Activities, with three methods for each, you will have 15 methods that are calling one another.

First, the CentralIntentFactory 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.

Also, here we have just one Activity, but we have two similar methods. It’s better to avoid this approach.

What about using factory methods inside my Activities like below?


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);
  }
  ...
}

You no longer have a big class, and you are not violating the Open/Closed Principle, but you are still writing a lot of boilerplate code.

Henson: Intent Builders

With Henson, you can create intents without writing any code using 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;  
  }
}

Here, is a constructor where we provide the context. We create intent, and with different methods, we can add the different input before building it.

Henson: Fluent API

You do not need to call intent builders yourself. With 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. With this, you can just generate your Intents without any specifically writing any code.

Henson: Annotations

With Activities and Intents, we are generating a navigation layer. We might want to use this navigation layer to navigate to all Activities even if they don’t have any extras.

Note that we have been 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.”

When we use Dart, the below 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, we use the HensonNavigable annotation, and then we provide a class that is a model.

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);
    ... 
}

Henson 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 compiles a 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.

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 dependency injection. This one is completely developed inside Groupon.

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. The gains you have are high, and you are avoiding writing out a lot of code, which means less effort.

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.

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

Daniel: If you are using that library just for this, you don’t need to use it anymore.

Next Up: New Features in Realm Java

General link arrow white

About the content

This talk was delivered live in March 2017 at DroidKaigi. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

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.

4 design patterns for a RESTless mobile integration »

close