Dependency Injection in Android with Dagger 2

Dẫn nhập

Chào mọi người, tiếp tục chủ đề về Dependency Inversion, Inversion of Control và Dependency Injection (DI), hôm nay tôi sẽ giới thiệu với các bạn một công cụ vô cùng mạnh mẽ để implement DI trong lập trình Android : Dagger2. Nếu các bạn chưa đọc về phần I, vui lòng xem qua một chút để hiểu rõ hơn về các khái niệm quan trọng trong Thiết kế hướng đối tượng tại đây nhé. Bắt đầu nào!!

Cài đặt

Integrate thư viện vào project Android

Dagger sẽ tự động generate code khi bạn build project, tuy nhiên, mặc định Android Studio không thừa nhận việc generate ra những đoạn code như vậy, và chúng ta sẽ add thêm plugin sau vào root build.gradle để giai quyết vấn đề đó.

  dependencies {
     // other classpath definitions here
     classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
 }

Thêm đoạn script sau vào app/build.gradle

dependencies {
    // apt command comes from the android-apt plugin
    apt 'com.google.dagger:dagger-compiler:2.7'
    compile 'com.google.dagger:dagger:2.7'
    provided 'javax.annotation:jsr250-api:1.0'
}

Dagger đang được hỗ trợ rất tốt nên các version sẽ được cập nhật thường xuyên, do đó các bạn cần chú ý đến version của các dependencies cũng như plugin sẽ được sử dụng.

Implementation

Tư tưởng của Dependency Injection là làm cho các module, class giao tiếp với nhau thông qua một abstract và abstract này sẽ được inject vào high-level class thông qua một Injector, hay nói cách các high-level class sẽ giao tiếp với low-level class thông qua một abstract mà không cần biết nó được khởi tạo như thế nào. Dagger rõ ràng được xây dựng dựa trên tư tưởng đó.

Các thành phần chính của Dagger

  1. Service: hay còn gọi là Module, là nơi cung cấp các abstract để inject vào high-level class.
  2. Client: là nơi sử dụng các Service được inject vào thông qua một Injector.
  3. DI container : thường được biết đến như là Object Graph, nơi cung cấp các Dependency (Service) sẽ được inject vào Client.

Annotation

Dagger sử dụng các annotation để implement nhằm tạo sự đơn gian cũng như tường minh của code.

  • @Scope: annotation định nghĩa vòng đời tồn tại của Object graph, nó thật sự hữu ích trong việc quản lý vòng đời của các Service sẽ được cung cấp cho Client. Chúng ta có thể define các loại Scope khác nhau như một annotation mới và sử dụng trong từng trường hợp cụ thể. Dưới đây là một ví dụ:
@Scope
@Documented
@Retention(value=RetentionPolicy.RUNTIME)
public @interface ActivityScope
{
}
  • @Module: annotation định nghĩa một Module, nơi sẽ cung cấp các service sẽ được inject vào Client.
  • @Provide: annotation được sử dụng trong một Module, định nghĩa các Service sẽ được cung cấp.
  • @Component: annotation định nghĩa một Component, là nơi sẽ expose ra toàn bộ các Service mà bạn dự định sẽ cung cấp cho Client.
  • @Inject: annotation được sử dụng ở Client, thông báo rằng service này sẽ được inject vào Client.
  • @Singleton: annotation sử dụng ở Module, đánh dấu Service được cung cấp dưới dạng một Singleton object.

Implementation

Chúng ta sẽ implement một Module, dùng để cung cấp các service giúp thao tác với API, parse dữ liệu trả về ... Ta sẽ hình dung các Service cần cung cấp đến client và các Service cần để làm dependency lẫn nhau.

@Module
public class NetModule {

    private static final String BASE_ENPOINT = "http://dummy.endpoint";

    // Dagger will only look for methods annotated with @Provides
    @Provides
    @Singleton
    // Application reference must come from AppModule.class
    SharedPreferences providesSharedPreferences(Application application) {
        return PreferenceManager.getDefaultSharedPreferences(application);
    }

    @Provides
    @Singleton
    Cache provideOkHttpCache(Application application) {
        int cacheSize = 10 * 1024 * 1024; // 10 MiB
        Cache cache = new Cache(application.getCacheDir(), cacheSize);
        return cache;
    }

   @Provides
   @Singleton
   Gson provideGson() {
       GsonBuilder gsonBuilder = new GsonBuilder();
       gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
       return gsonBuilder.create();
   }

   @Provides
   @Singleton
   OkHttpClient provideOkHttpClient(Cache cache) {
      OkHttpClient client = new OkHttpClient();
      client.setCache(cache);
      return client;
   }

   @Provides
   @Singleton
   Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) {
      Retrofit retrofit = new Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create(gson))
                .baseUrl(BASE_ENPOINT)
                .client(okHttpClient)
                .build();
        return retrofit;
    }
}

  • Ở ví dụ trên, ta cần một annotation @Module để khai báo một module mới. Trong module này, ta sẽ sử dụng @Provide để định nghĩa các Service sẽ được cung cấp và @Singleton để đánh dấu rằng các Service này sẽ được khởi tạo một lần. Rõ ràng, chúng ta thấy có những Service được định nghĩa để cung cấp cho Client (ví dụ như Retrofit), cũng có những service được định nghĩa để làm dependency cho các service khác (Gson, OkHttpClient, Cache...)

Chúng ta tiếp tục implement Component, là nơi sẽ expose ra các Service sẽ được client sử dụng.

@Singleton
@Component(modules={NetModule.class})
public interface NetComponent {
   Retrofit retrofit();
   Gson gson();
}

Component NetComponent sẽ expose cho client 2 service là RetrofitGson, khi sử dụng ở client, chúng ta sẽ inject nó vào bằng annotation @Inject, sẽ xem xét ở những ví dụ sau.

NetComponent sẽ provide các Service được sử dụng trong toàn app, do đó, scope của nó sẽ và Application hay Graph Object được tạo ra và tồn tại theo vòng đời của Application. Chúng ta tiến hành implement nó trong Application class.

public class MyApp extends Application {

    private NetComponent mNetComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        mNetComponent = DaggerNetComponent.builder()
                // list of modules that are part of this component need to be created here too
                .netModule(new NetModule())
                .build();
    }

    public NetComponent getNetComponent() {
       return mNetComponent;
    }
}

Chúng ta sẽ tiếp tục implement một Component khác, có dependency là NetComponent đã được định nghĩa ở trên. Component này sẽ được sử dụng như một Injector và cung cấp các Service cho client.

@ActivityScope
@Component(dependencies = NetComponent.class, modules= NetModule.class)
public interface AppComponent {
   void inject(MyActivity activity);
}

Annotation @ActivityScope được sử dụng để hạn chế scope của Object Graph, chỉ tồn tại trong vòng đời của một Activity. Ta sẽ tiến hành implement Client.

public class MyActivity extends Activity {
  @Inject Retrofit retrofit;
  @Inject Gson gson;

  @Overide
  public void onCreate(Bundle savedInstance) {
        DaggerAppComponent.builder()
                .netComponent(((MyApp)getApplication()).getNetComponent())
                .build().inject(this);

    }
}

Dagger sẽ tự động tìm kiếm trong Object graph của mình và inject vào Client các Service được đánh dấu bởi @Inject và chúng ta có thể sử dụng chúng mà không cần quan tâm đến cách khởi tạo như thế nào nhé, đó cũng chính là tư tưởng của DI.

Kết luận

Dagger 2 còn cung cấp cho chúng ta rất nhiều tính năng hay ho khác, nhưng trong giới hạn của bài viết, tôi chỉ đề cập đến những tính năng nổi bật nhất mà chúng ta có thể implement ngay trong ứng dụng của mình. Hy vọng rằng các bạn có thể sử dụng Dagger để xây dựng cho mình một ứng dụng "sạch" và "đẹp". Good luck!!