Sử dụng Dagger 2 cho sự phụ thuộc trên Android.

Nội dung

1. Giới thiệu khái niệm về sự phụ thuộc
2. Dependency injection with Dagger 2
3. Exercise: Dependency injection with Dagger 2
4. Dagger 2 and Android
5. Exercise: Dependency injection in Android activities with Dagger 2
6. Replacing @Module classes in tests
7. About this website
8. Dagger resources

1. Giới thiệu về sự phụ thuộc

  • Trong java, 1 lớp được coi là phụ thuộc vào lớp khác nếu nó sử dụng 1 thể hiện của lớp này.
  • Ví dụ: Ta có 2 lớp, lớp Motor và Vehicle, do lớp Vehicle có gọi lớp Motor nên Vehicle phụ thuộc vào lớp Motor
public class Motor {
 
    private int rpm;
 
    public Motor(){
        this.rpm = 0;
    }
 
    public int getRpm(){
        return rpm;
    }
 
    public void accelerate(int value){
        rpm = rpm + value;
    }
 
    public void brake(){
        rpm = 0;
    }
}

public class Vehicle {
 
    private Motor motor;
 
    public Vehicle(Motor motor){
        this.motor = motor;
    }
 
    public void increaseSpeed(int value){
        motor.accelerate(value);
    }
 
    public void stop(){
        motor.brake();
    }
 
    public int getSpeed(){
        return motor.getRpm();
    }
}

2. Sự phụ thuộc với Dagger 2

  • Dagger 2 là gì?

    – Dagger 2 là 1 framework về sự phụ thuộc. Tạo ra những đoạn code rất dễ đọc và dễ debug.

  • Các annotations của Dagger 2

    •  @Module và @Providers: Định nghĩa cho các lớp và các hàm nào là lớp phụ thuộc.
    • @Inject: Yêu cầu 1 sự phụ thuộc (Hàm khởi tạo, 1 trường hoặc 1 phương thức)
    • @Component: Là nối interface giữa các module và injection
    • Lưu ý: Không sử dụng injection cho các phương thức, trường private.

3. Ví dụ

4. Bắt đầu

Đầu tiên, chúng ta định nghĩa cấu trúc của 1 ứng dụng android. Bao gồm các lớp nền bên dưới

  1. DataManager: Cung cấp các truy cập dữ liệu đến kho dữ liệu cục bộ trên trong app.
  2. DbHelper: Là lớp quản lý SQLite và sẽ được gọi trong lớp DataManager.
  3. SharedPrefsHelper: Là lớp quản lý SharedPreferences và cũng được gọi trong lớp DataManager.
  4. Các Model class để lấy DB.

Bước 1:

  • Tạo ứng dụng mới và thêm thư viện
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'
}

Bước 2:

  • Tạo lớp User để tạo bảng trong SQLite
package laptrinh.live.dagger2.data.model;


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 + '\'' +
                '}';
    }
}

Bước 3:

  • Tạo các annotations tùy chỉnh

@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:

  • @Qualifier: Dùng để xác định đầy đủ, đôi khi mình sử dụng Application Context và Activity Context cả 2 điều sử dụng Context.
  • @Scope: được sử dụng để xác định phạm vi trong đó một đối tượng phụ thuộc vẫn tồn tại.

Bước 4:

Tạo lớp DbHelper kế thừa từ lớp SQLiteOpenHelper. Đây là lớp quản lý tất cả các bảng có liên quan tới SQLite.

package laptrinh.live.dagger2.data;

import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import javax.inject.Inject;
import javax.inject.Singleton;

import laptrinh.live.dagger2.data.model.User;
import laptrinh.live.dagger2.di.ApplicationContext;
import laptrinh.live.dagger2.di.DatabaseInfo;

/**
 * Created by MSI on 2/5/2018.
 */

@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 int version) {
        super(context, dbName, null, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        tableCreateStatements(db);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {
        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:

  • @Singleton: Đảm bảo chỉ có 1 thể hiện trên toàn bộ các lớp.
  • @Inject: Trên hàm khởi tạo để chỉ cho Dagger tích lũy các tham số phụ thuộc khi lớp được xây dựng.
  • @ApplicationContext Qualifier giúp cho DbHelper lấy đối tượng context trong ứng dụng.
  • @DatabaseInfo giúp cho Dagger phân biệt giữa String và Integer.

Bước 5:

package laptrinh.live.dagger2.data;

import android.content.SharedPreferences;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Created by MSI on 2/5/2018.
 */

@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: Lớp này cũng là lớp Singleton

Bước 6:

Tạo lớp DataManager

package laptrinh.live.dagger2.data;

import android.content.res.Resources;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.naming.Context;

import laptrinh.live.dagger2.data.model.User;
import laptrinh.live.dagger2.di.ApplicationContext;

/**
 * Created by MSI on 2/5/2018.
 */

@Singleton
public class DataManager {

    private Context mContext;
    private DbHelper mDbHelper;
    private SharedPrefsHelper mSharedPrefsHelper;

    @Inject
    public DataManager(@ApplicationContext Context mContext, DbHelper mDbHelper, SharedPrefsHelper mSharedPrefsHelper) {
        this.mContext = mContext;
        this.mDbHelper = mDbHelper;
        this.mSharedPrefsHelper = mSharedPrefsHelper;
    }

    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: Lớp này thể hiện sự phụ thuộc tới Context, DbHelper, SharedPrefsHelper trong hàm khởi tạo. Nó cung cấp tất cả các api để truy cập kho dữ liệu của ứng dụng.

Bước 7:

Tạo lớp DemoApplication

package laptrinh.live.dagger2;

import android.app.Application;
import android.content.Context;

import javax.inject.Inject;

import laptrinh.live.dagger2.data.DataManager;

/**
 * Created by MSI on 2/5/2018.
 */

public class DemoApplication extends Application {

    @Inject
    DataManager dataManager;

    public static DemoApplication get(Context context){
        return (DemoApplication)context.getApplicationContext();
    }


}

Notes: Thêm vào Manifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="laptrinh.live.dagger2">

    <application
        android:name=".DemoApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Bước 8:

Chúng ta phải cung cấp những phụ thuộc trong lớp DemoApplication. Chúng ta cung cấp DataManager và để cung cấp lớp này thì chúng ta phải cung cấp các phụ thuộc của DataManager là: Context, DbHelper và SharedPrefsHelper. Sau đó chúng ta cần cung cấp thêm các yêu cầu của các lớp đó.

  • Context phải là ApplicationContext
  • DbHelper cần Context, dbName, và Version. Và không có thêm nữa.
  • SharedPrefsHelper cần SharedPreferences

Bây giờ chúng ta cung cấp các phụ thuộc này cho DemoApplication

package laptrinh.live.dagger2.di.module;

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;


import dagger.Module;
import dagger.Provides;
import laptrinh.live.dagger2.di.ApplicationContext;
import laptrinh.live.dagger2.di.DatabaseInfo;

/**
 * Created by MSI on 2/5/2018.
 */

@Module
public class ApplicationModule {

    private Application mApplication;

    public ApplicationModule(Application application) {
        this.mApplication = application;
    }

    @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);
    }
}

Bước 9:

Chúng ta tạo ra 1 Component để liên kết 2 class DemoApplication và ApplicationModule

package laptrinh.live.dagger2.di.component;

import android.app.Application;
import android.content.SharedPreferences;

import javax.inject.Singleton;
import javax.naming.Context;

import dagger.Component;
import laptrinh.live.dagger2.DemoApplication;
import laptrinh.live.dagger2.data.DataManager;
import laptrinh.live.dagger2.data.DbHelper;
import laptrinh.live.dagger2.di.ApplicationContext;
import laptrinh.live.dagger2.di.module.ApplicationModule;

/**
 * Created by MSI on 2/5/2018.
 */

@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
    void inject(DemoApplication demoApplication);
    
    @ApplicationContext
    Context getContext();
    
    Application getApplication();
    
    DataManager getDataManager();
    
    SharedPreferences getSharedPreferencesHelper();
    
    DbHelper getDeHelper();
}

Notes: ApplicationComponent là 1 interface được implement từ Dagger2. Sử dụng @Component để chỉ rõ interface này là 1 conponent

Bước 11:

Tương tự, Chúng ta tạo module cho MainActivity và 1 Component.

package laptrinh.live.dagger2.di.module;

import android.app.Activity;
import android.content.Context;


import dagger.Module;
import dagger.Provides;
import laptrinh.live.dagger2.di.ActivityContext;

/**
 * Created by MSI on 2/5/2018.
 */

@Module
public class ActivityModule {
    private Activity mActivity;

    public ActivityModule(Activity mActivity) {
        this.mActivity = mActivity;
    }

    @Provides
    @ActivityContext
    Context provideContext(){
        return mActivity;
    }

    @Provides
    Activity provideActivity(){
        return mActivity;
    }
}

package laptrinh.live.dagger2.di.component;

import android.app.Activity;

import javax.inject.Singleton;

import dagger.Component;
import dagger.Module;
import laptrinh.live.dagger2.MainActivity;
import laptrinh.live.dagger2.di.PerActivity;
import laptrinh.live.dagger2.di.module.ActivityModule;

/**
 * Created by MSI on 2/5/2018.
 */

@PerActivity
@Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {
    void inject(MainActivity mainActivity);
}

Cập nhật lại lớp DemoApplication

package laptrinh.live.dagger2;

import android.app.Application;
import android.content.Context;

import javax.inject.Inject;

import dagger.internal.DaggerCollections;
import laptrinh.live.dagger2.data.DataManager;
import laptrinh.live.dagger2.di.component.ApplicationComponent;
import laptrinh.live.dagger2.di.component.DaggerApplicationComponent;
import laptrinh.live.dagger2.di.module.ApplicationModule;

/**
 * Created by MSI on 2/5/2018.
 */

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;
    }
}

Bước 12:

Tạo lớp MainActivity

package laptrinh.live.dagger2;

import android.os.PersistableBundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import javax.inject.Inject;

import laptrinh.live.dagger2.data.DataManager;
import laptrinh.live.dagger2.data.model.User;
import laptrinh.live.dagger2.di.component.ActivityComponent;
import laptrinh.live.dagger2.di.component.DaggerActivityConponent;
import laptrinh.live.dagger2.di.module.ActivityModule;

public class MainActivity extends AppCompatActivity {

    @Inject
    DataManager mDataManager;

    private TextView mTvUserInfo;
    private TextView mTvAccessToken;

    private ActivityComponent activityComponent;

    public ActivityComponent getActivityComponent(){
        if (activityComponent == null){
            activityComponent = DaggerActivityConponent.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
    public void onPostCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
        super.onPostCreate(savedInstanceState, persistentState);
        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();}
    }
}

Kết quả:

1