Dart Henson Library

 Introduction :

Dart and Henson are two dependency injection librarie similar to ButterKnife , for binding custom Bundle to explicite Intents,we use for Inter-Activities-Services-Brodcasts communication.Like all libraries first we should make an assessment on its cost before using it which leads to following question:

 

Is It expensive to use?

From this diagram the answer is Non for current version 2.0.2 (but expensive in previous version 1.0) , the size impacted on Dex and the number of Methods are clearly saying that it’s a soft and cheap Library.


Is it necessary?

Well it’s depending on your Application , if your are supposed to manage many navigation which require handling of communication via Intents , It may save your time.Otherwise from my opinion it’s useless!

 

What is the purpose ?Why Dart-Henson?

One of the pattern to setup a navigation in an Application is to use Intent through Activities , Intents are the unavoidable part of classical communication between Android Components except ContentProviders (Activities, Services, Broadcasts ), lovely startActivity is well known for all developers (even beginners) , we create an Intent and we pass it as an argument to startActivity there you go! That’s all ! Think about Intent as a huge data structure , some fields are pre-configured by system and some others are for you to configure,This Data Packet contains all information an Activity needs in order to start , so you configure your Intent then you send it to your target Activity (the one you want to start) when it’s about sending user data , all we have to do is to embed our data to this Intent, the classical way to do ,looks like this (Suppose from MainActivity you want to start another Activity =TargetActivity ) :

//Create Intent
Intent intent = new Intent(this,TargetActivity.class);

//Configure Intent
intent.putExtra(key1,value1);
intent.putExtra(key2,value2);

//Start target Activity
startActivity(intent);

 

Then on Target side (Started Activity) we can retrieve (restore) our data like that:

Bundle bundle = getIntent().getExtras();Object value = bundle.get(key)

You can think about bundle as the actual User Data Packet configured as a HashTable , it’s actually the container for all your stuff!
Notice that you are not allowed to put any kind of data (like a generic class) to Bundle , only some primitive or predefined data such as Int , Boolean,or String,… , Serializable, Parcellable , are authorized to be embedded to a Bundle, If you put a custom data class which is not conform to Serializable or Parcellable, System generate an Exception.

For every single navigation you have to send/restore your extra data like that in both STARTER and STARTED Activities.This way is particularly boring and may become a boilerplate soon ,everytime you navigate to an activity if data should be passed developers should setup and configure extra data the same way previous to call startActivity,It becomes a real annoying task particularly when your data to pass is relatively complex or a big parcellable parameters.

 

So can we simplify Intent binding process?

The answer is yes by using Dart injection library! Dart is an injection library similar to ButterKnife to help developers to avoid a messy boilerplate of putextra, getextra inside your activities or fragments.It  , to do that just add the extra data fields as follow:

👉@InjectExtra
👉 String extString;//variable for extra data , by default It is also used as the KEY 
👉@InjectExtra
👉 int extId;
//variable for extra data , by default It is also used as the KEY 

 

then call Dart.inject(this) in your onCreate() (onCreateView() in the case of Fragment)

for optional variable just let compiler know by adding @Nullable annotation to variable :

👉@Nullable
     @InjectExtra
     int extField;

Steps to setup injection

Suppose for our navigation purpose we want to start TargetActivity (Child) from MainActivity (Parent) by passing two parameters:

1.In Parent Activity (Master) :
Here for a very simple example we set two extra data, one a string value (extString) the second one an Integer value as identifier(extId) so somewhere in MainActivity we trigger this navigation like that:

startActivity(new Intent(this, TargetActivity.class)
.putExtra("extString","value)
.putExtra("extId",1));


 2.In Target Activity (Child = Detail):

we declare two fields(extString and extId) as extra data on top of your Activity class , then in onCreate we call Dart.inject():

public class TargetActivity extends AppCompatActivity {

👉@InjectExtra
👉 String extString;
👉@InjectExtra
👉 int extId;

@Override  protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
setContentView(R.layout.activity_target);

                  //Inject Dart
                  👉 Dart.inject(this);

        TextView tvExtra = (TextView)findViewById(R.id.txv_extra);        

        Bundle bundle = getIntent().getExtras();
    } }

By default Dart uses the fields name as the keys for Hashtable of those extra data so to restore/retrieve your data you can do:

👉 String str = Dart.get(bundle,"extString");
👉 int extraId = Dart.get(bundle,"extId");

Modelling Extra Injection :The good point is Dart has the ability to modelize Extra Injection in a class , so your Activity/Fragment gets more simplified by getting rid of all injected fields just create a class and move your fields there:

//Model class should be external ,Don’t declare inner!
public class DartModel {
    @InjectExtra
     String extString;
    @InjectExtra
     int extId;
}

For a more complex scenario we can also bind a Parcelable object to Injector Model like that:

public class DartModel {//Activity or Fragment A
    @InjectExtra
    String extStringA;    
    @InjectExtra 
    int extIdA;    
   @InjectExtra
    ExtParcelableData extParcelableData;//Parcelable data     ////Activity or Fragment B
    @InjectExtra
    String extStringB;
   @InjectExtra
    int extIdB;}

 

Our Parcelable class may look like this:

 public class ExtParcelableData implements Parcelable {

    private String extString;    private int extId;

    public String getExtraData(){           return extString+"--with Id="+String.valueOf(extId);

    }

   //And The rest of Boilerplate here}

Important :

If you are planning to use Parcelables or Serializable data , use explicitly an injector annotation in TargetActivity like that:

public class ChildActivityA extends AppCompatActivity {

@InjectExtra ExtParcelableData extParcelableData;
}

If you don’t use the injector in TargetActivity It may generates an Exception.
A good approach is to put all your DartModel and data class into the same package but externalize your Parcelable or Serializable class to not be an inner class of DartModel , DartModel should remain a mapping Model after all.

Notice:
The latest version of Dart library (V2.0.2) which this document is relayed on, supports also The Parcelable library https://github.com/johncarl81/parceler which is less boilerplate class for Parcelable objects but the price to use Parceler . class is expensive , take a look at Parcelor library , more than 705 methods and using around 78KB of memory just for replacing a traditional Parcelable class by a simpler version I'm not sure if it's a good choice!!


In this case In your onCreate you should call Dart.inject(modelInstance,Activity) variant instead of Dart.inject(Activity) like that:

@Override  protected
void 
onCreate(Bundle savedInstanceState) {  

super.onCreate(savedInstanceState);       

        //Inject Dart 

        👉 Dart.inject(new DartModel(),this);    } }

Example:

Suppose you have two Child Activities ChildActivityA , ChildActivityB and based on some conditions your MainActivity shouldstart one of them (see the picture below), so somewhere in your MainActivity :

if(statementA){    
      startActivity(new Intent(this,ChildActivityA.class)
      .putExtra("extStringA","valueA)      
      .putExtra("extIdA",1)
      .putExtra("extParcelableData", new ExtParcelableData(1,"othevalue")));

}else if(statementB){   
          startActivity(new Intent(this,ChildActivityB.class
          .putExtra("extStringB","valueB)  
          .putExtra("extIdB",2));
}

This is a classic and a basic way , but does it remain clean if we have more childs? In a realistic scenario with lots of navigation and Activities in a real App this portion of code gets bigger and probably once again a messy situation, so why not using a good Builder Pattern? Well it’s already done by Dart team , Henson is exactly what we want , Henson is another dependency that make the life easier , while Dart take care of Extra injections , Henson will be in charge of Intent creation in a smart way , you can inject all your extra data regardeless they are optional or not! Let’s see how it works:

Using Henson, Intents are managed entirely by Factory pattern builder ,we start Activity with just a simple line like that:

 startActivity(Henson.with(this)
                   .gotoChildActivityA()
                   .extString(“extStringA”)
                   .extId(1)
                   .build());

Henson.with(this) internally create a builder and detect automatically all TargetActivities and their extra fields declared in Target itself or DartModel,

Then you provide your extra data within this builder, in other word the intent for navigation is created as follow:

 

Intent navigationIntent = Henson.with(this
                                     .gotoChildActivityA()                     
                                     .extString(“extStringA”)
                                       .extId(1)           
                                       .build()


In the case you are using a Serializable/Parcelable object 
(for example in previous
ExtParcelableData) :

startActivity(Henson.with(this)       
                   .gotoChildActivityA()
                   .extParcelableData(new ExtParcelableData(6,"extString"))
                   .build()); 

Notice:
Android Studio is not aware of parent/child (Master/Detail) relationship in navigation You should let compiler knowabout this relationship by adding the following annotation in all TargetActivities the following:
@HensonNavigable

something like that:
@HensonNavigable
public class ChildActivityA extends AppCompatActivity {

}

Then rebuild your project so gotoChildActivityA will be constructed by Henson and recognized by Android Studio.


 A simple navigation scenario to show Buld-in Intent pattern in Henson 

 

 

In your Application you can either have separate Model class for each Activity/Fragment or ship your fields together in a single model class for all your Activities/Fragments like that:

public class DartModel {
    //Activity A (or Fragment A)
    
@InjectExtra
    String extStringA;
    @InjectExtra
    int extIdA;

    //Activity B (or Fragment B)
    
@InjectExtra
    String extStringB;
    @InjectExtra
    int extIdB;
}

 

Advantage / Desadvantage :

Good ponits:

  • Your Activity or Fragment will be offloaded the task of all Intent creation and  configuration, putExtra , getExtra,etc.. for each Activity/Fragment and so more cleaner.
  • Intents and extra parameters will be managed in a factory pattern.

Bad points:

  • The key for extra values still remain string in Dart annotation, a bad spelling or bad wording may lead to incorrect results.

 

Improve Implementation by Reflection:

As it’s underlined on Advantage/Desadvantage section, currently the only bad news about Dart & Henson is the “KEY” for extra value , you still have to call putExtra("KEY",value) or Dart.get(bundle,"KEY") which stay a source of potential error maybe defining the Keys as the final String contstants may resolve this behaviour but a good solution consists to implement some methods in DartModel class to convert class fields (injected fields) to string keys and expose them to developer so there is no need anymore to keep those key string constants and we can just pass field itself instead of KEY which is more safe . This conversion can be done safely by Reflection technic , a best approach is to apply this solution in Library itself rather than our DataModel.


How Add Library to your project:

  • First in your gradle file add annotation compiling option

defaultConfig {
        
javaCompileOptions {
            annotationProcessorOptions {    
            arguments = ['dart.henson.package''$applicationId']
            }      
      }   
 }
   

 

  • Add two compile dependencies ,the latest version at the time this document is created is V2.0.2:

dependencies {
compile 'com.f2prateek.dart:dart:2.0.2'
compile 'com.f2prateek.dart:henson:2.0.2'
provided 'com.f2prateek.dart:dart-processor:2.0.2'
provided 'com.f2prateek.dart:henson-processor:2.0.2'

}

 

April 24,2017
Khosrov

 © Xosrov 2016