Dependency injection với Dagger 2 - API

Bài viết này sẽ đi sâu vào tìm hiểu các nguyên tắc cơ bản của Dagger 2 và toàn bộ API của framework DI này.

Trong bài viết trước, chúng ta đã tìm hiểu về framework DI - có nhiệm vụ gắn kết, xâu chuỗi mọi thứ với nhau mà sử dụng ít code nhất có thể. Dagger 2 là một framework DI mà tạo ra rất nhiều boilerplate code cho chúng ta. Nhưng tại sao nó tốt hơn các framework DI khác? Thực chất, Dagger 2 là một framework DI tự động sinh ra đầy đủ code bắt chước lại code mà developer có thể tự viết. Tức là không hề có một phép thuật gì trong việc xây dựng đồ thị phụ thuộc. Dagger 2 kém linh động hơn so với các framework khác (không reflection tất cả) nhưng nó đơn giản và hiệu năng của code được sinh ra là tương tự như code viết tay.

Nguyên tắc căn bản của Dagger 2

Dưới đây là các API của Dagger 2:

public @interface Component {
    Class<?>[] modules() default {};
    Class<?>[] dependencies() default {};
}

public @interface Subcomponent {
    Class<?>[] modules() default {};
}

public @interface Module {
    Class<?>[] includes() default {};
}

public @interface Provides {
}

public @interface MapKey {
    boolean unwrapValue() default true;
}

public interface Lazy<T> {
    T get();
}

Ngoài ra còn có các thành phần khác được định nghĩa theo JSR-330 (chuẩn cho Dependency Injection trong Java) được sử dụng trong Dagger 2:

public @interface Inject {
}

public @interface Scope {
}

public @interface Qualifier {
}

Chúng ta hãy tìm hiểu tất cả về chúng:

@Inject annotation

Đầu tiên và quan trọng nhất của DI là @Inject annotation. Một phần của tiêu chuẩn JSR-330, đánh dấu những phụ thuộc mà phải được cung cấp bởi framework DI. Trong Dagger 2, có 3 cách khác nhau để cung cấp các phụ thuộc.

  • Constructor injection

@Inject được sử dụng với constructor của class:

public class LoginActivityPresenter {
    
    private LoginActivity loginActivity;
    private UserDataStore userDataStore;
    private UserManager userManager;
    
    @Inject
    public LoginActivityPresenter(LoginActivity loginActivity,
                                  UserDataStore userDataStore,
                                  UserManager userManager) {
        this.loginActivity = loginActivity;
        this.userDataStore = userDataStore;
        this.userManager = userManager;
    }
}

Tất cả các parameter được lấy từ đồ thị phụ thuộc (dependencies graph). @Inject anotation đã sử dụng trong constructor của class cũng làm cho class này trở thành một phần của đồ thị phụ thuộc. Có nghĩa là class này cũng có thể được inject khi cần thiết, chẳng hạn:

public class LoginActivity extends BaseActivity {

    @Inject
    LoginActivityPresenter presenter;
    
    //...
}

Giới hạn của cách làm này là chúng ta không thể khai báo annotation @Inject cho nhiều hơn một constructor trong class.

  • Fields injection

Một lựa chọn khác là khai báo annotaion @Inject cho các field:

public class SplashActivity extends AppCompatActivity {
    
    @Inject
    LoginActivityPresenter presenter;
    @Inject
    AnalyticsManager analyticsManager;
    
    @Override
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        getAppComponent().inject(this);
    }
}

Trong trường hợp này, quá trình injection phải được gọi cụ thể, chẳng hạn:

public class SplashActivity extends AppCompatActivity {
    
    //...
    
    @Override 
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        getAppComponent().inject(this);    //Requested depenencies are injected in this moment
    }
}

Trước lời gọi này, các phụ thuộc sẽ nhận giá trị null. Giới hạn của cách fields injection là chúng không thể được khai báo private. Tại sao? Bởi vì trong code được framework tự động sinh ra, các field này được gọi trực tiếp từ đối tượng, như sau:

//This class is generated automatically by Dagger 2
public final class SplashActivity_MembersInjector implements MembersInjector<SplashActivity> {

    //...

    @Override
    public void injectMembers(SplashActivity splashActivity) {
        if (splashActivity == null) {
            throw new NullPointerException("Cannot inject members into a null reference");
        }
        supertypeInjector.injectMembers(splashActivity);
        splashActivity.presenter = presenterProvider.get();
        splashActivity.analyticsManager = analyticsManagerProvider.get();
    }
}
  • Methods injection

Cách cuối cùng để cung cấp các phụ thuộc với @Inject là khai báo annotation cho public method của class:

public class LoginActivityPresenter {
    
    private LoginActivity loginActivity;
    
    @Inject 
    public LoginActivityPresenter(LoginActivity loginActivity) {
        this.loginActivity = loginActivity;
    }

    @Inject
    public void enableWatches(Watches watches) {
        watches.register(this);    //Watches instance required fully constructed LoginActivityPresenter
    }
}

Tất cả các tham số (parameter) của method được cung cấp bởi đồ thị phụ thuộc. Nhưng tại sao chúng ta cần method injection? Nó sẽ được sử dụng trong tình huống khi chúng ta muốn truyền vào một thể hiện của chính class này (tức là truyền vào this) để inject các phụ thuộc. Method injection được gọi ngay lập tức sau lời gọi khởi tạo constructor, tức là chúng ta đã truyền vào một đối tượng this đã được khởi tạo đầy đủ.

@Module annotation

@Module là một phần trong API của Dagger 2. Annotation này được sử dụng để đánh dấu, khai báo rằng class này là class cung cấp các phụ thuộc. Nhờ có class này, Dagger sẽ biết được nơi các đối tượng được khởi tạo.

@Module
public class GithubApiModule {
    
    @Provides
    @Singleton
    OkHttpClient provideOkHttpClient() {
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.setConnectTimeout(60 * 1000, TimeUnit.MILLISECONDS);
        okHttpClient.setReadTimeout(60 * 1000, TimeUnit.MILLISECONDS);
        return okHttpClient;
    }

    @Provides
    @Singleton
    RestAdapter provideRestAdapter(Application application, OkHttpClient okHttpClient) {
        RestAdapter.Builder builder = new RestAdapter.Builder();
        builder.setClient(new OkClient(okHttpClient))
               .setEndpoint(application.getString(R.string.endpoint));
        return builder.build();
    }
}

@Provides annotation

Annotation này được sử dụng trong @Module class. @Provides sẽ đánh dấu các method trong Module mà return các phụ thuộc.

@Module
public class GithubApiModule {
    
    //...
    
    @Provides   //This annotation means that method below provides dependency
    @Singleton
    RestAdapter provideRestAdapter(Application application, OkHttpClient okHttpClient) {
        RestAdapter.Builder builder = new RestAdapter.Builder();
        builder.setClient(new OkClient(okHttpClient))
               .setEndpoint(application.getString(R.string.endpoint));
        return builder.build();
    }
}

@Component annotation

Annotation này được sử dụng để xây dựng các interface dùng để xâu chuỗi, gắn kết mọi thức với nhau. Tại đây, chúng ta xác định các module (hoặc các component khác) sẽ là nguồn cung cấp các đối tượng phụ thuộc. Ngoài ra, tại đây cũng xác định các đồ thị phụ thuộc hay có thể hiểu là các đối tượng phụ thuộc nào sẽ có thể được inject và xác định nơi các đối tượng sẽ được inject vào. @Component lúc này đóng vai trò như cầu nối giữa @Module@Inject. Ví dụ, @Component sử dụng 2 module, có thể inject sự phụ thuộc vào GithubClientApplication và tạo ra 3 đối tượng phụ thuộc được công khai:

@Singleton
@Component(
    modules = {
        AppModule.class,
        GithubApiModule.class
    }
)
public interface AppComponent {

    void inject(GithubClientApplication githubClientApplication);

    Application getApplication();

    AnalyticsManager getAnalyticsManager();

    UserManager getUserManager();
}

@Component cũng có thể phụ thuộc vào component khác, và được xác định vòng đời, như sau:

@ActivityScope
@Component(      
    modules = SplashActivityModule.class,
    dependencies = AppComponent.class
)
public interface SplashActivityComponent {
    SplashActivity inject(SplashActivity splashActivity);

    SplashActivityPresenter presenter();
}

@Scope annotation

@Scope
public @interface ActivityScope {
}

@Scope là một thành phần khác của tiêu chuẩn JSR-330. Trong Dagger 2, @Scope được sử dụng để định nghĩa phạm vi tuỳ chỉnh. Phần này sẽ được trình bày thành một chủ đề trong bài viết sau.

Ví dụ áp dụng

Bây giờ, chúng ta sẽ áp dụng các kiến thức trên vào thực tế. Chúng ta sẽ cài đặt một ứng dụng Github client đơn giản sử dụng Dagger 2. Ứng dụng sẽ có 3 Activity và một use case đơn giản theo flow sau:

  1. Gõ vào username Github
  2. Nếu user tồn tại thì hiển thị danh sách các repository công khai
  3. User click vào danh sách trên thì hiển thị thông tin chi tiết của repository đc click đó

Các màn hình ứng dụng như sau:

Phần DI sẽ như sau:

Mỗi Activity sẽ có một đồ thị phụ thuộc của riêng mình. Mỗi đồ thị (tức là _Component class) có 2 đối tượng là _Presenter_Activity. Ngoài ra, mỗi component còn có phụ thuộc từ global component - tức là AppComponent, nó cung cấp các đối tượng Application, UserManagerRepositoriesManager.

AppComponent có cấu trúc như sau:

AppComponent bao gồm 2 module là AppModuleGithubApiModule.

Kết luận

Trong bài viết trước, chúng ta đã biết được lợi ích của việc sử dụng DI. Và trong bài viết này, một framework DI khá hiệu quả được giới thiệu đó là Dagger 2. Trên đây là toàn bộ API được đặc tả để sử dụng với Dagger 2.