Lately, it is hard to browse through Android tutorials without an article on Dagger 2 or MVP staring 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:
- 6 table spoons of dark coffee
- 1 cup of water
- 1 table spoon of Splenda (a type of sweetener) and
- 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 recipe was defined with concrete ingredients. We can however update our coffee recipe with generic ingredients like this:
- 6 table spoons of coffee
- 1 cup of water
- 1 table spoon of sweetener
- 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 an average and two, Android is a complex web of dependencies. The code we write often depends 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 are 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
- 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. - At the root of the project, create the following class files:
- ShoppingCart.java – We have talked about this class already, so it should be obvious what is its mission. Next is
- 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. - At the root of the project, create a package called
dagger
and add the following class files to the package
- AppComponent.java –
Component
is one of the three legs upon which Dagger 2 stands, this is simply an interface that does two things:
- It lists the Dagger Modules in this app.
- 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
.
- AppModule.java –
Module
is another one of the three legs 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 dependencies. This particular module provides the Context
. - ShoppingCartModule.java – This class is another module that will provide – you guessed it instances of
ShoppingCart
.
- At the root of the application, add a package called
model
and add the following class files to this package.
- Product.java – This defines the products that the app will sell
- LineItem.java – This extends product to add price
- At the root of the project, add file called ProntoShopApplication.java – this will extend from Application.
- Update the name of the Application in Manifest.xml to reflect the name of the class you just added ProntoShopApplication.java
- The source code for this sample project can be found here.
- 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:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
- In your module build.gradle file, add the highlighted lines of code:
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
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'
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'
}
- Build your project.
- Remember to update your
applicationId
in case you just copied the above line of code and paste.
Step 3: Update Models
- 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();
}
- 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 revolvers, however before they can be deployed, Dagger 2 needs to know which objects are inject-able targets and it needs 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() {
}
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 {
@Inject
ShoppingCart mCart;
public ProductListener(){
ProntoShopApplication.getInstance().getAppComponent().inject(this);
}
public void onItemQuantityChanged(LineItem item, int qty) {
mCart.updateItemQty(item, qty);
}
public void onAddToCartButtonClicked(Product product) {
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 a 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 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.
The post Learn Android Dependency Injection with Dagger 2 appeared first on Val Okafor.