Pular para o conteúdo principal

Dagger2 Beginners Tutorial and Examples

Dependency injection is an important concept in software development, particularly in Android development. Dagger2 is a dependency injection framework for Android that helps manage dependencies between objects. In this article, we will cover the basics of Dagger2 and how to implement it in your Android application.

What is Dependency Injection?

Dependency injection is a technique in software engineering where the dependencies of an object are injected into it rather than being created within the object itself. This technique helps to improve the modularity and maintainability of the code. Dependencies can be any object that is required for the functioning of another object.

What is Dagger2?

Dagger2 is a dependency injection framework for Android. It is an implementation of the Dependency Injection pattern and is designed to help manage the dependencies between objects in an Android application. Dagger2 is a compile-time framework that generates code that is used to provide dependencies to objects at runtime.

Getting Started with Dagger2

To use Dagger2 in your Android application, you need to add it to your project as a dependency. Add the following dependency to your app-level build.gradle file:

dependencies {
implementation 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

Replace 2.x with the latest version of Dagger2.

Understanding Dagger2 Annotations

Dagger2 uses annotations to identify and create dependencies. Here are some of the annotations used by Dagger2:

  • @Inject: Used to mark a constructor or a field with a dependency that needs to be injected.

  • @Component: Used to identify the interface that Dagger2 uses to generate the code that provides dependencies.

  • @Module: Used to identify the class that provides the dependencies.

  • @Provides: Used to identify the method that provides the dependency.

Creating a Simple Example

Let's create a simple example to understand how Dagger2 works. We will create a class Car which needs an instance of Engine to function.

class Engine {
fun start() {
println("Engine started")
}
}

class Car(private val engine: Engine) {
fun start() {
engine.start()
println("Car started")
}
}

In this example, Car needs an instance of Engine to function. We can create an instance of Engine and pass it to Car, but this becomes difficult when there are multiple dependencies. This is where Dagger2 comes in.

First, we need to mark the Engine constructor with @Inject to let Dagger2 know that it needs to be injected.

class Engine @Inject constructor() {
fun start() {
println("Engine started")
}
}

Next, we need to define a @Module class that provides the Engine dependency.

@Module
class CarModule {
@Provides
fun provideEngine(): Engine = Engine()
}

In this module, we have defined a method provideEngine() that provides an instance of Engine.

Now, we need to create a @Component interface that connects the Car and CarModule.

@Component(modules = [CarModule::class])
interface CarComponent {
fun inject(car: Car)
}

In this interface, we have defined the CarModule as a module and Car as an object that needs to be injected.

Finally, we can create an instance of CarComponent and use it to inject Car with the Engine dependency.

fun main() {
val carComponent = DaggerCarComponent.create()
val car = Car(Engine())
carComponent.inject(car)
car.start()
}

In this example, we have created an instance of CarComponent using DaggerCarComponent.create() and used it to inject an instance of Car. We can now call the start() method on Car to start the engine.

Dagger2 Best Practices

Here are some best practices recommended in the Android Documentation regarding Dagger2

  • Use constructor injection with @Inject to add types to the Dagger graph whenever it's possible. When it's not:
    • Use @Binds to tell Dagger which implementation an interface should have.
    • Use @Provides to tell Dagger how to provide classes that your project doesn't own.
  • You should only declare modules once in a component.
  • Name the scope annotations depending on the lifetime where the annotation is used. Examples include @ApplicationScope, @LoggedUserScope, and @ActivityScope.

More Dagger2 Examples.

There are two examples: one in Java then one in Kotlin.

1. Hello World Dagger2 Example with SharedPreferences.

In this tutorial we want to introduce Dagger2 practically with a real world project. We will use the SharedPreferences to:

  1. Save Data From Edittext to SharedPreferences.
  2. Retrieve Data From SharedPreferences onto an EditText.

Our user interface comprises two edittexts where the user enters a username and his number and then clicks the save button. Then we get the value when the user clicks a get button. We rebind the value in the edittext.

Let's start.

Here's the demo:

Android Dagger2 Example

(a). Build.gradle

We start by proceeding to our app level build.gradle and adding our dependencies. Dagger2 is a third party library maintained by Google. We have to fetch it from internet so we start by adding it's dependency via Gradle.

  1. Once you've created your android application in android studio, navigate over to the build.gradle located in the app folder and move to the dependencies DSL.
  2. Replace them with the following code. You can replace the versions with the latest versions.
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.google.dagger:dagger:2.16'
annotationProcessor 'com.google.dagger:dagger-compiler:2.16'
}

Here are the dependencies we've added:

  1. appcompat- A support library which will give us access to the AppCompatActivity class.
  2. constraint-layout - To give us the constraint layout.
  3. dagger2 - Our third party dependency injection framework. We also add our annotationProcessor. Remember Dagger2 is annotation based framework and we will be using several annotations to annotate our classes, interfaces, methods and fields.
(b). activity_main.xml

Next we come to activity_main.xml. This layout typically gets inflated into the MainActivity. In this layout we will construct our user interface using several elements all which are indirect or direct subclasses of the View class.

They include

  1. LinearLayout- This will allow us arrange items linearly. In this case it is our container layout and we arrange it's children vertically.
  2. TextView - Just to display our header.
  3. ImageView - To render our dummy image.
  4. EditText - To provide an interface for users to type data to be saved into our SharedPreferences.
  5. Button - When clicked we will either save to SharedPreferences or retrieve from them.

Here's the full code:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout

android_layout_width="match_parent"
android_layout_height="match_parent"
android_orientation="vertical"
android_background="#009688"
tools_context=".MainActivity">

<TextView
android_id="@+id/headerTxt"
android_layout_width="match_parent"
android_layout_height="wrap_content"
android_text="Spiritual Teachers App"
android_textAlignment="center"
android_fontFamily="casual"
android_textStyle="bold"
android_textAppearance="@style/TextAppearance.AppCompat.Large"
android_textColor="@color/white" />

<ImageView
android_layout_width="match_parent"
android_layout_height="300dp"
android_layout_centerHorizontal="true"
android_src="@drawable/logo"
android_contentDescription="@string/app_name"/>

<RelativeLayout
android_layout_width="match_parent"
android_layout_height="wrap_content">

<EditText
android_id="@+id/usernameTxt"
android_layout_width="match_parent"
android_layout_height="wrap_content"
android_layout_margin="8dp"
android_textColor="@color/white"
android_hint="Username"
/>

<EditText
android_id="@+id/numberTxt"
android_layout_width="match_parent"
android_layout_height="wrap_content"
android_layout_below="@+id/usernameTxt"
android_layout_margin="8dp"
android_inputType="number"
android_textColor="@color/white"
android_hint="Number"
/>

<Button
android_id="@+id/saveBtn"
android_layout_width="wrap_content"
android_layout_height="wrap_content"
android_text="SAVE"
android_layout_below="@+id/numberTxt"
android_layout_toLeftOf="@+id/getBtn"
android_layout_toStartOf="@+id/getBtn"
android_layout_marginRight="8dp"
android_layout_marginEnd="8dp"
/>

<Button
android_id="@+id/getBtn"
android_layout_width="wrap_content"
android_layout_height="wrap_content"
android_text="GET"
android_layout_below="@+id/numberTxt"
android_layout_alignRight="@+id/numberTxt"
android_layout_alignEnd="@+id/numberTxt"
/>
</RelativeLayout>

</LinearLayout>
(d). SharedPrefModule.java

This is our module class. Dagger2 has two main concepts, a module and a component. Basicly the module represents our dependencies while component represents a mapping between the module and it's client/dependent object.

So we come create a module class here. But first we have to start by importing several objects:

  1. Context - required by our SharedPreferences class.
  2. SharedPreferences - We will return it's instance as a dependency to the dependant.
  3. PreferenceManager - Will provide us our SharedPreference object.
  4. Singleton - will mark our object as a singleton. A singleton is a shared object. You can have one instance of a singleton object in memory.
  5. Module - will mark our SharedPrefModule class a module.
  6. Provides - Will mark our provider methods.

Here are the methods we create:

  1. SharedPrefModule(Context context) : Our constructor. Will take a Context object and assign it to a local instance field.
  2. provideContext() - a method that wil return us a Context object if needed as a dependency.
  3. provideSharedPreferences() - a method that will return us a SharedPreferences object as a dependency to the dependant.

Here's the full code. Take not that we mark our methods with Singleton and Provides annotations.

package info.camposha.mrdagger;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

import javax.inject.Singleton;

import dagger.Module;
import dagger.Provides;

/**
* Let's create our module. This will be mapped to our activity by the Component.
* We will pass it a Context object via the constructor. Then it will provide that
* Context as well as the SharedPreferences as our dependencies.
*/
@Module
public class SharedPrefModule {
private Context context;

public SharedPrefModule(Context context) {
this.context = context;
}

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

@Singleton
@Provides
public SharedPreferences provideSharedPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
}
(d). MyComponent.java

This is our Dagger Component interface. A Component is a class that provides the mapping between a Module and a Dependent object. In this case will be mapping our SharedPrefModule to our MainActivity. So MainActivity is the dependnat object. It depends on the Module which is the SharedPrefModule. From it can get two methods injected into it as dependencies. Those are methods marked with @Provides annotation.

Here are some of our imports:

  1. Component - This is an annotation that will mark our interface as a daggger component.

Here are the annotations used:

  1. @Singleton - To mark our interface implementation object as a singleton. Only a single instance of that object can exist in memory.
  2. @Component - To mark our interface as a component. We provide it our modules/module. In this case we have only a single module. Suppose we had many we would have separated them with commas.

Here are our methods:

  1. void inject(..) - Will receive our dependant and map it to our module.

Here's the full code:

package info.camposha.mrdagger;

import javax.inject.Singleton;
import dagger.Component;

/**
* Create our DaggerComponent. It is a singleton.
* It will map our dependency which is our module to our dependent which is the
* <code>MainActivity</code>.
*/
@Singleton
@Component(modules = {SharedPrefModule.class})
public interface MyComponent {
void inject(MainActivity activity);
}
(e). MainActivity.java

This is our MainActivity class. This will be our launcher activity. It is registered on the android manifest as it is an android component. Here are the things we will do here:

  1. Set our contentview for this activity.
  2. Initialize widgets like Buttons and Edittexts.
  3. Get data from edittext and insert them into shared preference.
  4. Retrieve data from sharedpreferences and bind them to edittexts.
  5. Initialize our DaggerComponent and pass it this activity's instance.

[notice] Rebuild you project so that Dagger2 can generate us the DaggerMyComponent class. [/notice]

That DaggerMyComponent is our MyComponent implementation. We will build it and pass it our module. Then we also inject into it our dependant.

Building The Component

Here's the method responsible for that:

    private void initializeDaggerComponent(){
myComponent = DaggerMyComponent
.builder()
.sharedPrefModule(new SharedPrefModule(this))
.build();
myComponent.inject(this);
}

How to save to SharedPreferences

Here's the method for that

    private void saveToSharedPreference(){
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("username", usernameTxt.getText().toString().trim());
editor.putString("number", numberTxt.getText().toString().trim());
editor.apply();
}
package info.camposha.mrdagger;

import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import javax.inject.Inject;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

EditText usernameTxt, numberTxt;
Button saveBtn, getBtn;
private MyComponent myComponent;
@Inject
SharedPreferences sharedPreferences;

/**
* Initialize DaggerComponent and inject it our activity
*/
private void initializeDaggerComponent(){
myComponent = DaggerMyComponent
.builder()
.sharedPrefModule(new SharedPrefModule(this))
.build();
myComponent.inject(this);
}

/**
* Initialize Buttons and EditTexts
*/
private void initializeViews() {
getBtn = findViewById(R.id.getBtn);
saveBtn = findViewById(R.id.saveBtn);
usernameTxt = findViewById(R.id.usernameTxt);
numberTxt = findViewById(R.id.numberTxt);

saveBtn.setOnClickListener(this);
getBtn.setOnClickListener(this);
}

/**
* Save to shared preference.
* <code>SharedPreferences.Editor</code> is an interface used for modifying values in a
* SharedPreferences object.
* It's putString() will Set a String value in the preferences editor, to be
* written back once commit() or apply() are called.
*/
private void saveToSharedPreference(){
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("username", usernameTxt.getText().toString().trim());
editor.putString("number", numberTxt.getText().toString().trim());
editor.apply();
}

/**
* Retrieve From SharedPreferences and Bind to EditText
*/
private void retrieveFromSharedPreferences(){
usernameTxt.setText(sharedPreferences.getString("username", "Default"));
numberTxt.setText(sharedPreferences.getString("number", "12345"));
}

/**
* When the buttons are clicked
* @param view
*/
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.getBtn:
retrieveFromSharedPreferences();
break;
case R.id.saveBtn:
saveToSharedPreference();
break;
}

}
/**
* Our onCreate method
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initializeViews();
initializeDaggerComponent();
}
}

Example 2: Kotlin Android Dagger2 Example

Here's another dead simple Dagger2 example written in Kotlin.

Step 1: Install Dagger2

Start by installing Dagger2 in your build.gradle, by specifying the dependency statement as follows:

    implementation 'com.google.dagger:dagger:2.24'
kapt 'com.google.dagger:dagger-compiler:2.24'

At the top of the app/build.gradle do not forget to apply the kapt plugin:

apply plugin: 'kotlin-kapt'

Step 2: Design Layout

Add a TextView to your main XML layout:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.elyeproj.simplestappwithdagger2.MainActivity">

<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="48sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Step 3: Write Code

Go to your main activity and add imports as follows. You may use AndroidX:

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import dagger.Component
import kotlinx.android.synthetic.main.activity_main.*
import javax.inject.Inject

Create a Component known as MagicBox as an interface:

@Component
interface MagicBox {
fun poke(app: MainActivity)
}

Then a class called info.Annotate the constructor with the @Inject property:

class Info @Inject constructor() {
val text = "Hello Dagger 2"
}

Then use them as follows in the MainActivity:

class MainActivity : AppCompatActivity() {

@Inject lateinit var info: Info

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerMagicBox.create().poke(this)
text_view.text = info.text
}
}

Here is the full code

MainActivity.kt

package com.elyeproj.simplestappwithdagger2

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import dagger.Component
import kotlinx.android.synthetic.main.activity_main.*
import javax.inject.Inject

class MainActivity : AppCompatActivity() {

@Inject lateinit var info: Info

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerMagicBox.create().poke(this)
text_view.text = info.text
}
}

class Info @Inject constructor() {
val text = "Hello Dagger 2"
}

@Component
interface MagicBox {
fun poke(app: MainActivity)
}

Reference

Download the code here

Conclusion

In this article, we have covered the basics of Dagger2 and how to use it to manage dependencies in an Android application. We have also created a simple example to help you understand how Dagger2 works. Dagger2 is a powerful framework that can help you write cleaner, more modular, and maintainable code.