Learn Android Dependency Injection with Dagger 2

Lately, it is hard to browse through Android tutorials without an article on Dagger 2 or MVP starring at you. In this post, I will share with you a practical definition and example of Dagger 2 and in an upcoming post, I will share with you a to the point example of MVP.

What is Dependency Injection

At a basic level, Dependency Injection encourages flexibility. The term was coined by Martin Fowler in an article about Inversion of Control pattern. A simple example of Dependency Injection is recipes – take a recipe for coffee for example. You can define the ingredients for a cup of coffee like this:

  1. 6 table spoon of dark coffee,
  2. 1 cup of water,
  3. 1 table spoon of Splenda (a type of sweetener) and
  4. 1 teaspoon of vanilla flavored cream.

This recipe can work in most cases, however if someone prefers a caramel flavored cream, a new set of recipe has to be made and if someone prefers a different type of sweetener likewise a new recipe has to be made because the original recipes was defined with concrete ingredients. We can however update our coffee recipe with generic ingredients like this:

  1. 6 table spoon of coffee,
  2. 1 cup of water,
  3. 1 table spoon of sweetener
  4. 1 teaspoon of cream

This now makes our recipe flexible, the actual type of cream or sugar used will be determined or rather injected at the time the coffee is made. This gives the flexibility to for example use mock ingredients during training and switch to real ingredients during production.

The power of dependency injection shines with (sticking with the coffee analogy) if you now want to make cappuccino which requires or depends on some coffee input. The recipe for the cappuccino can be based on the flexible coffee recipe.

Yet another special drink can also be made which requires or depends on cappuccino. So that means three layers of dependencies. With Dependency Injection pattern, the generic ingredients, will be replaced (or injected) with concrete ingredients by the bartender just in time before serving the coffee.

How does this apply to Android Development

This applies to Android development in two ways: one we as developers drink lots of coffee on average and two Android is a complex web of dependencies. The code we write often depend on libraries which has dependencies on other libraries.

Context and SharedPreference are good examples of this as we will see shortly in the example that comes with this tutorial. Without Dependency Injection, you are limited to using SharedPreference in only files that inherit from Context aka Activity and Fragments because new instance of SharedPreference has a dependency on Context.

Without getting into MVP in this post, I can say that it makes life easy if you extract your application logic including data persistence from Fragments and Activities into POJO classes aka Presenters. Since these POJO classes do not have Context, how do you get access to SharedPreference which has a dependency on Context? The answer is Dependency Injection – specifically we use Dagger 2 to inject SharedPreference to any class that needs it as you will see in the example that follows. So let us take a look at Dagger 2 next.

What Exactly does Dagger 2 Do

Before we look at what exactly Dagger 2 does, I will like to say that the good thing about Dagger 2 is that it is not magical. Everything it does, at-least the part that you are concerned with is prescriptive. That means you prescribe to Dagger 2 what goes where via annotations. So what does Dagger 2 do? It is a factory that creates and manages your dependency for you based on your specification.

Going back to the coffee analogy, Dagger 2 can be likened to a vending machine that stands near the bartender and creates new copies of the concrete ingredient needed by the bartender to make coffees based on the checklist provided to it.

The checklist must be clear, for example if it specifies that when the recipe calls for 1 table spoon of cream, it should also say what should be the concrete implementation that should be created to satisfy that requirement. Dagger 2 does not make the decision of what object to use to satisfy a dependency for you, that is why I said that it is not magical. You have to specify that upfront. Let’s see this in an example.

Dagger 2 By Example

In this example we are going to create a Java class file called ShoppingCart.java, this file is used in an Android shopping cart app. Part of the work of this ShoppingCart class file is to remember the items that the users add the shopping cart in case there is a configuration change aka phone call came in before they complete their shopping session by checking out. In the web there is cookies where this kind of temp information can be stored but in Android we have SharedPreference for such a time like this.

Since this ShoppingCart.java file is a humble plain old Java object (POJO) we need to use Dagger 2 to inject SharedPreference to it. Follow the steps below to complete the tutorial. If you are feeling fired up about Dagger 2 and want to see other practical examples, you may want to be notified about my upcoming course Pronto SQLite.

Step 1 – New Project Creation

  1. Create a brand new Android, you can also add this to an existing Android Studio project if you choose to. I am going to name my project ProntoShopApp and select the defaults and click finish.
  2. At the root of the project, create the following class files

1. ShoppingCart.java – we have talked about this class already, so it should be obvious what is its mission. Next is
2. ProductPresenter.java – yet another mention of Presenter, don’t worry I will cover MVP in another post, but just know that it is nothing but a Java class file.
3. At the root of the project, create a package called dagger and add the following class files to the package

1. AppComponent.java – Component is one of the three legs upon which Dagger 2 stands, this is simply an interface that does two things:

1. It lists the Dagger Modules in this app.
2. It lists injectable targets, for example we need SharedPreference in our ShoppingCart.java class, so it is an injectable object that we must specify in the AppComponent.

2. AppModule.java – Module is another one of the three leg that Dagger 2 stands upon. This is a standard Java class that will be decorated with Dagger 2 specific annotations and it is the methods in this class that provide the dependecies. This particular module provides the Context.
3. ShoppingCartModule.java – this class is another module what will provide – you guessed it instances of ShoppingCart.

3. At the root of the application add a package called model and add the following class files to this package

1. Product.java – this defines the products that we will the app will sell
2. LineItem.java – this extends product to add price

4. At the root of the project add file called ProntoShopApplication.java – this will extend from Application.

5. Update the name of the Application in Manifest.xml to reflect the name of the class you just added ProntoShopApplication.java

6. The Source code for this sample project can be found here.

7. Share – If you have found this post useful, please share with someone who can benefit from it.

Step 2 – Add Dagger 2

In your Project build.gradle, add the following line – classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’ this will help Android Studio to recognize Dagger 2 generated code. Your Project gradle file should now look like this:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

2. In your module build.gradle file, add the highlighted lines of code

apply plugin: 'com.android.application'
<strong>apply plugin: 'com.neenbedankt.android-apt'</strong>
android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.okason.prontoshopapp"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.android.support:design:23.2.1'
    <strong>apt 'com.google.dagger:dagger-compiler:2.2'
    compile 'com.google.dagger:dagger:2.2'
    provided 'javax.annotation:jsr250-api:1.0'
    compile 'com.google.code.gson:gson:2.6.2'</strong>
}

3. Build your project

4. Remember to update your applicationId in case you just copied the above line of code and paste.

Step 3: Update Models

  1. Update Product.java with the following code and then use Android Studio Code->Generate->Getter and Setter to generate the boiler plate getter/setter code.
 private long id;
    private String productName;
    private String description;   
    private double salePrice;


    public Product(){
    }

    public Product(Product product){
        this.id = product.getId();
        this.productName = product.getProductName();
        this.description = product.getDescription();
        this.salePrice = product.getSalePrice();       

    }

2. Update LineItem.java with the following code and then also use Android Studio to generate the required boiler plate getter/setter code.

public class LineItem extends Product {
    private int quantity;


    public LineItem() {
    }

    public LineItem(Product product, int quantity) {
        super(product);
        this.quantity = quantity;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    public double getSumPrice() {
        return getSalePrice() * quantity;
    }
}

Step 4: AppModule Class

Update your AppModule.java class with the code below. We are now beginning to leverage Dagger 2. Make sure that your ProntoShopApplication.java extends the Application class. Notice the @Module annotation at the top of the class. That is what signifies to Dagger 2 that this is a Module. And what do Modules do again you may ask. Modules in Dagger 2 contain methods which are essentially advanced switch statements. So here we are saying, case Context: return the Application class. It is best practices to name those method names that begin with provide.

@Module
public class AppModule {
    private final ProntoShopApplication app;

    public AppModule(ProntoShopApplication app) {
        this.app = app;
    }


    @Provides @Singleton
    public Context provideContext() {
        return app;
    }
}

Step 5: Update the ShoppingCart Module

Same with the AppModule.java we are annotating this class with @Module annotation. We are saying wherever in our application that we need the ShoppingCart, create a new instance of the ShoppingCart and since the ShoppingCart has a dependency on SharedPreference go ahead and satisfy that dependency by asking the PreferenceManager. And since the PreferenceManager has a dependency on Context before it can fulfill the request for a SharedPreference, Dagger 2 now knows to go to our AppModule and satisfy that dependency by returning our Application class.

@Module
public class ShoppingCartModule {

    @Provides @Singleton
    SharedPreferences providesSharedPreference(Context context){
        return PreferenceManager.getDefaultSharedPreferences(context);
    }

    @Provides @Singleton
    ShoppingCart providesShoppingCart(SharedPreferences preferences){
        return  new ShoppingCart(preferences);
    }


}

Don’t worry about the red lines in Shopping Cart, we will address that shortly.

Step 6: Update the Injector Class

Modules are potential dependency resolvers, however before they can be deployed Dagger 2 needs to know which objects are inject-able targets and it need to get a list of Modules that are providing dependency solutions. We provide these two information in the AppComponent.java class like so: Notice that it has the list of our two modules and the classes that we may need a dependency to be injected.

@Singleton
@Component(
        modules = {
                AppModule.class,
                ShoppingCartModule.class
        }
)
public interface AppComponent {
    void inject(ProductListener presenter);    
    void inject(MainActivity activity);

}

Step 7: Update the Shopping class

Your Cart logic could be as complex as your business needs demands, here is a sample implementation that demonstrates saving the cart content to the SharedPreference.

public class ShoppingCart {
    private final SharedPreferences sharedPreferences;
    private SharedPreferences.Editor editor;
    private List<LineItem> shoppingCart;

    private static final String OPEN_CART_EXITS = "open_cart_exists";
    private static final String SERIALIZED_CART_ITEMS = "serialized_cart_items";
    private static final String SERIALIZED_CUSTOMER = "serialized_customer";



    public ShoppingCart(SharedPreferences sharedPreferences) {
        this.sharedPreferences = sharedPreferences;
        editor = sharedPreferences.edit();
        initShoppingCart();
    }

    private void initShoppingCart() {
        shoppingCart = new ArrayList<>();

        Gson gson = new Gson();

        if (sharedPreferences.getBoolean(OPEN_CART_EXITS, false)){
            String serializedCartItems = sharedPreferences.getString(SERIALIZED_CART_ITEMS,"");
            String serializedCustomer = sharedPreferences.getString(SERIALIZED_CUSTOMER,"");
            if (!serializedCartItems.equals("")){
                shoppingCart = gson.<ArrayList<LineItem>>fromJson(serializedCartItems,
                        new TypeToken<ArrayList<LineItem>>(){}.getType());
            }


        }
        updateApp();


    }

    public void addItemToCart(LineItem item){
        if (shoppingCart.contains(item)){
            int currentPosition = shoppingCart.indexOf(item);
            LineItem itemAlreadyInCart = shoppingCart.get(currentPosition);
            itemAlreadyInCart.setQuantity(itemAlreadyInCart.getQuantity() + item.getQuantity());
            shoppingCart.set(currentPosition, itemAlreadyInCart);
        }else {
            shoppingCart.add(item);
        }

    }

    public void clearShoppingCart(){
        shoppingCart.clear();
        editor.putString(SERIALIZED_CART_ITEMS, "").commit();
        editor.putString(SERIALIZED_CUSTOMER, "").commit();
        editor.putBoolean(OPEN_CART_EXITS, false).commit();
        updateApp();
    }

    public void removeItemFromCart(LineItem item){
        shoppingCart.remove(item);
        updateApp();
    }


    public void completeCheckout(){
        shoppingCart.clear();
        updateApp();
    }

    private void updateApp() {
       //perform any action that is needed to update the app
    }

    public List<LineItem> getShoppingCart() {
        return shoppingCart;
    }



    public void saveCartToPreference(){
        if (shoppingCart != null) {
            Gson gson = new Gson();
            String serializedItems = gson.toJson(shoppingCart);
            editor.putString(SERIALIZED_CART_ITEMS, serializedItems).commit();
            editor.putBoolean(OPEN_CART_EXITS, true).commit();
        }
    }

    public void updateItemQty(LineItem item, int qty) {

        boolean itemAlreadyInCart = shoppingCart.contains(item);

        if (itemAlreadyInCart) {
            int position = shoppingCart.indexOf(item);
            LineItem itemInCart = shoppingCart.get(position);
            itemInCart.setQuantity(qty);
            shoppingCart.set(position, itemInCart);
        } else {
            item.setQuantity(qty);
            shoppingCart.add(item);
        }

        updateApp();

    }
}

Step 8: Update the Application Class

This is the last and very important step, if you copy the code below and paste into your application class you will get compiler error. Why? Because the DaggerComponent has not been created. You remember that our AppComponent is nothing but an Interface, and you remember that the power of an interface lies in the implementation class. Dagger creates the concrete implementation using the @ annotations that we supplied and adds the prefix to Dagger to them so our AppComponent which is an interface will now have a concrete implementation called DaggerAppComponent.

Update your Application class like so:

public class ProntoShopApplication extends Application {

        private static ProntoShopApplication instance = new ProntoShopApplication();
        private static AppComponent appComponent;

        public static ProntoShopApplication getInstance() {
            return instance;
        }

        @Override
        public void onCreate() {
            super.onCreate();
            getAppComponent();
        }

        public AppComponent getAppComponent() {
            if (appComponent == null){
                appComponent = DaggerAppComponent.builder()
                        .appModule(new AppModule(this))
                        .build();
            }
            return appComponent;
        }

}

Step 9 – Put Dagger 2 to Good Use

Now that we have everything wired up, we can actually use Dagger 2 like this.

public class ProductListener {

    //We are creating a class member variable for the 
    //Shopping cart that we will be injecting to this class
    @Inject
    ShoppingCart mCart;
    
    public ProductListener(){
        //Here is where the actual injection takes place
        ProntoShopApplication.getInstance().getAppComponent().inject(this);
    }

    //Here is an example of how we are using the injected shopping cart
    public void onItemQuantityChanged(LineItem item, int qty) {
        mCart.updateItemQty(item, qty);      
    }

    //Another example of using the shopping cart
    public void onAddToCartButtonClicked(Product product) {
        //perform add to checkout button here
        LineItem item = new LineItem(product, 1);
        mCart.addItemToCart(item);
    }

    public void onClearButtonClicked() {
        mCart.clearShoppingCart();
    }

    public void onDeleteItemButtonClicked(LineItem item) {
        mCart.removeItemFromCart(item);       
    }
}

Conclusion

There you have it, a practical introduction to Dagger 2 dependency injection framework. This is barely scratching the surface on the immense possibilities that this tool provides. The post should help you get started using Dagger and you will get clarity as you pound the keyboard because of code answereth all things.

If you have found value in this post, please use the social media buttons to share this post as it may benefit another Android developer. The source code for this sample project can be found here.

Leave a Comment

Your email address will not be published. Required fields are marked *