Introduction to Dagger 2, Using Dependency Injection in Android: Part 2
This is the part 2 of the article series. In part 1 we understood the need and advantages of dependency injection. We also got an overview of Dagger 2. In this part, we will focus on implementing the DI using Dagger 2 in an android app.
Check Part 1 here.
For the sake of this tutorial, we will break the process in steps and analyze each step one by one. Remember Dagger requires a concentrated approach. So actively follow the below tutorial, asking a lot of questions. For the project structure, see the project repo mentioned below.
let’s get started.
The GitHub repo for the project: https://github.com/janishar/android-dagger2-example
First, we have to define the structure of the Android App. The core classes are as follows:
class will provide access to the data of the application.DataManager
class will be used by DataManager to access the SQLite database.DbHelper
will be used bySharedPrefsHelper
to access theDataManager
.SharedPreferences
- Model classes for retrieving the DB table.
Step 1:
Create a project in android studio with an empty Activity and add the following dependencies in app’s build.gradle
dependencies {
...
compile "com.google.dagger:dagger:2.8"
annotationProcessor "com.google.dagger:dagger-compiler:2.8"
provided 'javax.annotation:jsr250-api:1.0'
compile 'javax.inject:javax.inject:1'
}
Notes: We are using the annotation processor provided by gradle for android. dagger-compiler is the annotation processing repo for generating the dependency graph classes during build time. Other gradle dependencies are provided for the sake of Dagger.
Step 2:
The data part we will be building first. So, create a model class
.User
public class User {
private Long id;
private String name;
private String address;
private String createdAt;
private String updatedAt;
public User() {
}
public User(String name, String address) {
this.name = name;
this.address = address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", address='" + address + '\'' +
", createdAt='" + createdAt + '\'' +
", updatedAt='" + updatedAt + '\'' +
'}';
}
}
Notes: This class will bind the DB table data.
Step 3:
Create few custom annotations:
, ActivityContext
, ApplicationContext
, DatabaseInfo
PerActivity
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityContext {
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationContext {
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseInfo {
}
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}
Notes:
Why are we creating these annotations and what is
and @Qualifier
?@Scope
annotation is provided by javax inject package and is used to qualify the dependency. For example, a class can ask both, an Application Context and an Activity Context. But both these Objects will be of type Context. So, for Dagger to figure out which variable is to be provided with what, we have to explicitly specify the identifier for it.@Qualifier
Thus
is used to distinguish between objects of the same type but with different instances. In the above code, we have @Qualifier
and ActivityContext
so that the ApplicationContext
object being injected can refer to the respective Context
type. Context
is used to provide the database name in the class dependency. Since a DatabaseInfo
class in being provided as a dependency, it always a good idea to qualify it so that the Dagger can explicitly resolve it.String
An alternative to this is
annotation provided by Dagger. @Named
itself is annotated with @Named
. With @Qualifier
we have to provide string identifier for similar class objects and this identifier is used to map the dependency of a class. We will explore the @Named
at the end of this example.@Named
is used to specify the scope in which a dependency object persists. If a class getting dependencies, have members injected with classes annotated with a scope, then each instance of that class asking for dependencies will get its own set of member variables.@Scope
Step 4:
Create a
class that extends the DbHelper
. This class will be responsible for all the DB related operations.SQLiteOpenHelper
@Singleton
public class DbHelper extends SQLiteOpenHelper {
//USER TABLE
public static final String USER_TABLE_NAME = "users";
public static final String USER_COLUMN_USER_ID = "id";
public static final String USER_COLUMN_USER_NAME = "usr_name";
public static final String USER_COLUMN_USER_ADDRESS = "usr_add";
public static final String USER_COLUMN_USER_CREATED_AT = "created_at";
public static final String USER_COLUMN_USER_UPDATED_AT = "updated_at";
@Inject
public DbHelper(@ApplicationContext Context context,
@DatabaseInfo String dbName,
@DatabaseInfo Integer version) {
super(context, dbName, null, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
tableCreateStatements(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + USER_TABLE_NAME);
onCreate(db);
}
private void tableCreateStatements(SQLiteDatabase db) {
try {
db.execSQL(
"CREATE TABLE IF NOT EXISTS "
+ USER_TABLE_NAME + "("
+ USER_COLUMN_USER_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ USER_COLUMN_USER_NAME + " VARCHAR(20), "
+ USER_COLUMN_USER_ADDRESS + " VARCHAR(50), "
+ USER_COLUMN_USER_CREATED_AT + " VARCHAR(10) DEFAULT " + getCurrentTimeStamp() + ", "
+ USER_COLUMN_USER_UPDATED_AT + " VARCHAR(10) DEFAULT " + getCurrentTimeStamp() + ")"
);
} catch (SQLException e) {
e.printStackTrace();
}
}
protected User getUser(Long userId) throws Resources.NotFoundException, NullPointerException {
Cursor cursor = null;
try {
SQLiteDatabase db = this.getReadableDatabase();
cursor = db.rawQuery(
"SELECT * FROM "
+ USER_TABLE_NAME
+ " WHERE "
+ USER_COLUMN_USER_ID
+ " = ? ",
new String[]{userId + ""});
if (cursor.getCount() > 0) {
cursor.moveToFirst();
User user = new User();
user.setId(cursor.getLong(cursor.getColumnIndex(USER_COLUMN_USER_ID)));
user.setName(cursor.getString(cursor.getColumnIndex(USER_COLUMN_USER_NAME)));
user.setAddress(cursor.getString(cursor.getColumnIndex(USER_COLUMN_USER_ADDRESS)));
user.setCreatedAt(cursor.getString(cursor.getColumnIndex(USER_COLUMN_USER_CREATED_AT)));
user.setUpdatedAt(cursor.getString(cursor.getColumnIndex(USER_COLUMN_USER_UPDATED_AT)));
return user;
} else {
throw new Resources.NotFoundException("User with id " + userId + " does not exists");
}
} catch (NullPointerException e) {
e.printStackTrace();
throw e;
} finally {
if (cursor != null)
cursor.close();
}
}
protected Long insertUser(User user) throws Exception {
try {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(USER_COLUMN_USER_NAME, user.getName());
contentValues.put(USER_COLUMN_USER_ADDRESS, user.getAddress());
return db.insert(USER_TABLE_NAME, null, contentValues);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
private String getCurrentTimeStamp() {
return String.valueOf(System.currentTimeMillis() / 1000);
}
}
Notes: This class introduces few annotation.
ensure a single instance of a class globally. So, there will be only one@Singleton
DbHelper
class instance for the app and whenever a class asks for
as a dependency, it will be provided with the same instance that is maintained in the Dagger’s dependency graph.DbHelper
on the constructor instructs the Dagger to accumulate all the parameter dependencies when the class is being constructed.@Inject
@ApplicationContext
facilitatesQualifier
to get the context object of the application from dagger’s dependency graphDbHelper
qualifier helps the dagger to distinguish between@DatabaseInfo
andString
Dependencies from existing same types in the dependency graph.Integer
We will again come back to this discussion when we deal with the module.
Rest of the contents of this class is standard
. This class creates a SQLiteOpenHelper
table and inserts/reads it.user
Step 5:
Create a
to deal with the shared preferences.SharedPrefsHelper
@Singleton
public class SharedPrefsHelper {
public static String PREF_KEY_ACCESS_TOKEN = "access-token";
private SharedPreferences mSharedPreferences;
@Inject
public SharedPrefsHelper(SharedPreferences sharedPreferences) {
mSharedPreferences = sharedPreferences;
}
public void put(String key, String value) {
mSharedPreferences.edit().putString(key, value).apply();
}
public void put(String key, int value) {
mSharedPreferences.edit().putInt(key, value).apply();
}
public void put(String key, float value) {
mSharedPreferences.edit().putFloat(key, value).apply();
}
public void put(String key, boolean value) {
mSharedPreferences.edit().putBoolean(key, value).apply();
}
public String get(String key, String defaultValue) {
return mSharedPreferences.getString(key, defaultValue);
}
public Integer get(String key, int defaultValue) {
return mSharedPreferences.getInt(key, defaultValue);
}
public Float get(String key, float defaultValue) {
return mSharedPreferences.getFloat(key, defaultValue);
}
public Boolean get(String key, boolean defaultValue) {
return mSharedPreferences.getBoolean(key, defaultValue);
}
public void deleteSavedData(String key) {
mSharedPreferences.edit().remove(key).apply();
}
}
Notes: This class is annotated with
to make this a singleton class in the dependency graph of Dagger.@Singleton
This class also gets
dependency through Dagger which is expressed by the SharedPreferences
on the constructor.@Inject
How is this dependency provided? It is explained later in this example.
Step 6:
Create
classDataManager
@Singleton
public class DataManager {
private Context mContext;
private DbHelper mDbHelper;
private SharedPrefsHelper mSharedPrefsHelper;
@Inject
public DataManager(@ApplicationContext Context context,
DbHelper dbHelper,
SharedPrefsHelper sharedPrefsHelper) {
mContext = context;
mDbHelper = dbHelper;
mSharedPrefsHelper = sharedPrefsHelper;
}
public void saveAccessToken(String accessToken) {
mSharedPrefsHelper.put(SharedPrefsHelper.PREF_KEY_ACCESS_TOKEN, accessToken);
}
public String getAccessToken(){
return mSharedPrefsHelper.get(SharedPrefsHelper.PREF_KEY_ACCESS_TOKEN, null);
}
public Long createUser(User user) throws Exception {
return mDbHelper.insertUser(user);
}
public User getUser(Long userId) throws Resources.NotFoundException, NullPointerException {
return mDbHelper.getUser(userId);
}
}
Notes: This class expresses the dependencies of Application
, Context
and DbHelper
in the contructor. It provides all the apis to access the data of the application.SharedPrefsHelper
Step 7:
Create
class that extends DemoApplication
android.app.Application
public class DemoApplication extends Application {
protected ApplicationComponent applicationComponent;
@Inject
DataManager dataManager;
public static DemoApplication get(Context context) {
return (DemoApplication) context.getApplicationContext();
}
@Override
public void onCreate() {
super.onCreate();
applicationComponent = DaggerApplicationComponent
.builder()
.applicationModule(new ApplicationModule(this))
.build();
applicationComponent.inject(this);
}
public ApplicationComponent getComponent(){
return applicationComponent;
}
}
Add this class in AndroidManifest.xml
<application
...
android:name=".DemoApplication"
...
</application>
Notes: This class gets
through DI. The interesting part in this class is the DataManager
. Let’s go on with the steps before we cover this point.ApplicationComponent
Step 8:
Create class MainActivity
public class MainActivity extends AppCompatActivity {
@Inject
DataManager mDataManager;
private ActivityComponent activityComponent;
private TextView mTvUserInfo;
private TextView mTvAccessToken;
public ActivityComponent getActivityComponent() {
if (activityComponent == null) {
activityComponent = DaggerActivityComponent.builder()
.activityModule(new ActivityModule(this))
.applicationComponent(DemoApplication.get(this).getComponent())
.build();
}
return activityComponent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getActivityComponent().inject(this);
mTvUserInfo = (TextView) findViewById(R.id.tv_user_info);
mTvAccessToken = (TextView) findViewById(R.id.tv_access_token);
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
createUser();
getUser();
mDataManager.saveAccessToken("ASDR12443JFDJF43543J543H3K543");
String token = mDataManager.getAccessToken();
if(token != null){
mTvAccessToken.setText(token);
}
}
private void createUser(){
try {
mDataManager.createUser(new User("Ali", "1367, Gurgaon, Haryana, India"));
}catch (Exception e){e.printStackTrace();}
}
private void getUser(){
try {
User user = mDataManager.getUser(1L);
mTvUserInfo.setText(user.toString());
}catch (Exception e){e.printStackTrace();}
}
}
Also create activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_user_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:padding="10dp"/>
<TextView
android:id="@+id/tv_access_token"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:padding="10dp"/>
</LinearLayout>
Now let’s hold on for some time and review the Dagger.
- To provide the dependency for a class we have to create a Module class. This class defines the methods that provide the dependency. A Module class is identified by
and the dependency provider method in identified by@Module
.@Provides
- We then have to create an interface that serves as a connection between a class that expresses the dependency through
and a class that provides the dependency i.e. annotated with@Inject
.@Module
- In order to figure out the dependencies that a Module has to provide, we have to scan all the classes in the graph that needs to be provided with dependencies and then figure out the least number of objects that has to be provided.
Let’s move back to the example steps to understand the above statements.
Step 9:
We have to provide the dependencies expressed in the
class. This class needs DemoApplication
and to provide this class we have to provide the dependencies expressed by DataManager
, which as mentioned in the constructor are DataManager
, Context
, and DbHelper
. We then move further in the graph.SharedPrefsHelper
has to beContext
ApplicationContext
needsDbHelper
,Context
, anddbName
. This does not have any further branching.version
needsSharedPrefsHelper
SharedPreferences
We now accumulate the superset of all these dependencies, which turn out to be:
, Context
, dbName
, and version
SharedPreferences
Now to provide these dependencies we create ApplicationModule
@Module
public class ApplicationModule {
private final Application mApplication;
public ApplicationModule(Application app) {
mApplication = app;
}
@Provides
@ApplicationContext
Context provideContext() {
return mApplication;
}
@Provides
Application provideApplication() {
return mApplication;
}
@Provides
@DatabaseInfo
String provideDatabaseName() {
return "demo-dagger.db";
}
@Provides
@DatabaseInfo
Integer provideDatabaseVersion() {
return 2;
}
@Provides
SharedPreferences provideSharedPrefs() {
return mApplication.getSharedPreferences("demo-prefs", Context.MODE_PRIVATE);
}
}
Note: We have annotated this class with
and all the methods with @Module
.@Provides
Let’s explore this class:
- In the constructor, we have passed the
instance. This instance is used to provide other dependencies.Application
- This class provides all the dependencies that we listed in the above step.
Step 10:
We create a Component which links the
dependency and the DemoApplication
ApplicationModule
@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
void inject(DemoApplication demoApplication);
@ApplicationContext
Context getContext();
Application getApplication();
DataManager getDataManager();
SharedPrefsHelper getPreferenceHelper();
DbHelper getDbHelper();
}
Notes:
is an interface that is implemented by Dagger. Using ApplicationComponent
we specify the class to be a Component.@Component
Here we have written a method
where we pass the inject
instance. Why do we do it?DemoApplication
When the dependencies are provided through field injection i.e.
on the member variables, we have to tell the Dagger to scan this class through the implementation of this interface.@inject
This class also provides methods that are used to access the dependencies that exist in the dependency graph.
Step 11:
Similarly, we have to create the module for
and it’s component. Which follow the same procedures as in the above step.MainActivity
@Module
public class ActivityModule {
private Activity mActivity;
public ActivityModule(Activity activity) {
mActivity = activity;
}
@Provides
@ActivityContext
Context provideContext() {
return mActivity;
}
@Provides
Activity provideActivity() {
return mActivity;
}
}
@PerActivity
@Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {
void inject(MainActivity mainActivity);
}
Note:
specify ActivityComponent
and ApplicationComponent
. ActivityModule
is added to use the graph that has already been generated in the previous step and do exists because the ApplicationComponent
class persists till the application is running.DemoApplication
is a scope and is used to tell the Dagger that the @PerActivity
and Context
provided by the Activity
will be instantiated each time an ActivityModule
is created. So, these objects persist till that activity lives and each activity has its own set of these.Activity
We may ask that the
will then be created with each DataManager
. But that is not true because we have annotated the Activity
class with DataManager
. Which brings the class in the global scope and thus is accessed whenever a dependency is expressed.@Singleton
Now let’s revisit the
class and DemoApplication
class. These classes don’t have a constructor and Android System is responsible for instantiating these. To get the dependency we use the MainActivity
method as it is called once when they are instantiated.OnCreate
applicationComponent = DaggerApplicationComponent
.builder()
.applicationModule(new ApplicationModule(this))
.build();
applicationComponent.inject(this);
is the generated class by the Dagger, implementing the DaggerApplicationComponent
. We provide the ApplicationComponent
class that is used to construct the dependencies.ApplicationModule
We have also called the
method of inject
and passed the instance of the applicationComponent
class. This is done to use it for providing the DemoApplication
instance is retained so as to access all the classes that are available in the dependency graph and is express for access.DataManager
.ApplicationComponent
public ActivityComponent getActivityComponent() {
if (activityComponent == null) {
activityComponent = DaggerActivityComponent.builder()
.activityModule(new ActivityModule(this))
.applicationComponent(DemoApplication.get(this).getComponent())
.build();
}
return activityComponent;
}
Similar process is applied in the
as well. The only difference is that, we also provide MainActivity
that is required for dependencies resolution mentioned in ApplicationComponent
.ActivityModule
This completes the Example project.
Hope you have got a working knowledge of Dagger.
The GitHub repo for this example project: https://github.com/janishar/android-dagger2-example
As we mentioned about
annotation, we just have to replace @Named("string")
and @ApplicationContext
with something like @ActivityContext
and @Named("application_context")
every where. But personally I don’t like this implementation. It requires to maintain a @Named("activity_context")
tag.String
Note: If due to some reason we have to inject dependencies through field injection for classes other that android constructed classes then define a component interface for that class and call it’s inject method in class’s constructor. I would suggest to figure out a way not to do this but try to use the constructor with
for the injection.@Inject