Dùng Dagger trong các ứng dụng nhiều mô-đun

Một dự án có nhiều mô-đun Gradle được gọi là một dự án đa mô-đun. Trong một dự án nhiều mô-đun được gửi dưới dạng một tệp APK duy nhất không có mô-đun tính năng, thông thường sẽ có một mô-đun app có thể phụ thuộc vào hầu hết các mô-đun của dự án và một mô-đun base hoặc core mà phần còn lại của các mô-đun thường phụ thuộc vào. Mô-đun app thường chứa lớp Application, trong khi mô-đun base chứa tất cả các lớp chung được chia sẻ trên tất cả các mô-đun trong dự án của bạn.

Mô-đun app là vị trí phù hợp để khai báo thành phần ứng dụng của bạn (ví dụ như ApplicationComponent trong hình bên dưới), nơi có thể cung cấp các đối tượng mà các thành phần khác có thể cần cũng như các singleton của ứng dụng. Chẳng hạn như các lớp OkHttpClient, trình phân tích cú pháp JSON, trình truy cập cho cơ sở dữ liệu của bạn, hoặc các đối tượng SharedPreferences có thể được xác định trong mô-đun core, sẽ được cung cấp bởi ApplicationComponent đã xác định trong mô-đun app.

Trong mô-đun app, bạn cũng có thể sở hữu các thành phần khác có thời gian tồn tại ngắn hơn. Ví dụ như UserComponent với cấu hình dành riêng cho người dùng (như UserSession) sau khi đăng nhập.

Trong các mô-đun khác nhau của dự án, bạn có thể xác định ít nhất một thành phần phụ có logic dành riêng cho mô-đun đó như minh họa trong hình 1.

Hình 1. Ví dụ về biểu đồ Dagger trong một dự án nhiều mô-đun

Ví dụ: trong mô-đun login, bạn có thể có phạm vi LoginComponent với chú thích @ModuleScope tuỳ chỉnh có thể cung cấp các đối tượng chung cho tính năng đó, chẳng hạn như LoginRepository. Bên trong mô-đun đó, bạn cũng có thể có các thành phần khác phụ thuộc vào LoginComponent với một phạm vi tuỳ chỉnh khác, ví dụ: @FeatureScope cho LoginActivityComponent, hoặc TermsAndConditionsComponent nơi bạn có thể đặt phạm vi logic cụ thể hơn cho tính năng, chẳng hạn như các đối tượng ViewModel.

Bạn cũng cần thiết lập tương tự đối với các mô-đun khác như Registration.

Quy tắc chung cho một dự án nhiều mô-đun là các mô-đun cùng cấp không được phụ thuộc lẫn nhau. Nếu có, hãy cân nhắc xem logic dùng chung (các phần phụ thuộc giữa chúng) có phải là một phần của mô-đun mẹ hay không. Nếu có, hãy tái cấu trúc để di chuyển các lớp vào mô-đun mẹ; nếu không, hãy tạo một mô-đun mới mở rộng mô-đun mẹ và để cả hai mô-đun ban đầu mở rộng mô-đun mới.

Cách tốt nhất là bạn nên tạo một thành phần trong một mô-đun ở các trường hợp sau:

  • Bạn cần thực hiện chèn trường, như với LoginActivityComponent.

  • Bạn cần đặt phạm vi cho các đối tượng, như với LoginComponent.

Nếu cả hai trường hợp này đều không áp dụng được và bạn cần cho Dagger biết cách cung cấp các đối tượng từ mô-đun đó, hãy tạo và hiển thị một mô-đun Dagger bằng các phương thức @Provides hoặc @Binds nếu không thể chèn nội dung xây dựng cho các lớp đó.

Triển khai bằng các thành phần phụ của Dagger

Trang tài liệu Sử dụng Dagger trong các ứng dụng Android trình bày cách tạo và sử dụng các thành phần phụ. Tuy nhiên, bạn không thể sử dụng cùng một mã vì các mô-đun tính năng không biết về mô-đun app. Chẳng hạn như nếu bạn nghĩ về một luồng Đăng nhập thông thường và mã chúng tôi có ở trang trước, thì quy trình này sẽ không biên dịch nữa:

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    // Creation of the login graph using the application graph
    loginComponent = (applicationContext as MyDaggerApplication)
                        .appComponent.loginComponent().create()

    // Make Dagger instantiate @Inject fields in LoginActivity
    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Creation of the login graph using the application graph
        loginComponent = ((MyApplication) getApplicationContext())
                                .appComponent.loginComponent().create();

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this);

        ...
    }
}

Lý do là mô-đun login không biết về MyApplicationappComponent. Để làm cho nó hoạt động, bạn cần xác định một giao diện trong mô-đun tính năng cung cấp FeatureComponentMyApplication cần triển khai.

Ở ví dụ sau, bạn có thể xác định giao diện LoginComponentProvider cung cấp LoginComponent trong mô-đun login của luồng Đăng nhập:

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

Giờ thì LoginActivity sẽ sử dụng giao diện đó thay vì đoạn mã được xác định ở trên:

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    loginComponent = (applicationContext as LoginComponentProvider)
                        .provideLoginComponent()

    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loginComponent = ((LoginComponentProvider) getApplicationContext())
                                .provideLoginComponent();

        loginComponent.inject(this);

        ...
    }
}

Hiện tại, MyApplication cần triển khai giao diện đó cùng với các phương thức bắt buộc:

Kotlin

class MyApplication: Application(), LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  val appComponent = DaggerApplicationComponent.create()

  override fun provideLoginComponent(): LoginComponent {
    return appComponent.loginComponent().create()
  }
}

Java

public class MyApplication extends Application implements LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  ApplicationComponent appComponent = DaggerApplicationComponent.create();

  @Override
  public LoginComponent provideLoginComponent() {
    return appComponent.loginComponent.create();
  }
}

Đây là cách bạn có thể sử dụng các thành phần phụ của Dagger trong một dự án nhiều mô-đun. Giải pháp này khác với các mô-đun tính năng do cách các mô-đun phụ thuộc lẫn nhau.

Các phần phụ thuộc của thành phần với mô-đun tính năng

Với mô-đun tính năng, cách các mô-đun thường phụ thuộc vào nhau bị đảo ngược. Thay vì mô-đun app bao gồm các mô-đun tính năng, các mô-đun tính năng lại phụ thuộc vào mô-đun app. Xem hình 2 để biết cách cấu trúc các mô-đun.

Hình 2. Ví dụ về biểu đồ Dagger trong một dự án có các mô-đun tính năng

Trong Dagger, các thành phần cần biết về các thành phần phụ của chúng. Thông tin này có trong mô-đun Dagger được thêm vào thành phần mẹ (như mô-đun SubcomponentsModule ở bài viết Sử dụng Dagger trong các ứng dụng Android).

Thật không may, với phần phụ thuộc bị đảo ngược giữa ứng dụng và mô-đun tính năng, thành phần phụ không thể hiển thị ở mô-đun app vì nó không nằm trong đường dẫn của bản dựng. Ví dụ như LoginComponent đã xác định trong mô-đun tính năng login không thể là thành phần phụ của ApplicationComponent được xác định trong mô-đun app.

Dagger có một cơ chế được gọi là phần phụ thuộc của thành phần mà bạn có thể dùng để giải quyết vấn đề này. Thay vì thành phần con là một thành phần phụ của thành phần mẹ, thì thành phần con phụ thuộc vào thành phần mẹ. Theo đó, không có mối quan hệ mẹ con; hiện tại các thành phần phụ thuộc vào thành phần khác để có được phần phụ thuộc nhất định. Các thành phần cần hiển thị các loại có trong biểu đồ để các thành phần phụ thuộc sử dụng chúng.

Ví dụ như một mô-đun tính năng có tên là login muốn tạo một LoginComponent phụ thuộc vào AppComponent có sẵn trong mô-đun Gradle app.

Dưới đây là định nghĩa cho các lớp và AppComponent thuộc mô-đun Gradle app:

Kotlin

// UserRepository's dependencies
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

// UserRepository is scoped to AppComponent
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Singleton
@Component
interface AppComponent { ... }

Java

// UserRepository's dependencies
public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    @Inject
    public UserRemoteDataSource() { }
}

// UserRepository is scoped to AppComponent
@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

@Singleton
@Component
public interface ApplicationComponent { ... }

Trong mô-đun gradle login bao gồm mô-đun gradle app, bạn có một LoginActivity cần chèn vào bản sao của LoginViewModel:

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

LoginViewModel có một phần phụ thuộc trên UserRepository có sẵn và trong phạm vi AppComponent. Hãy tạo một LoginComponent phụ thuộc vào AppComponent để chèn LoginActivity vào:

Kotlin

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = [AppComponent::class])
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity loginActivity);
}

LoginComponent chỉ định một phần phụ thuộc trên AppComponent bằng cách thêm phần phụ thuộc đó vào tham số phần phụ thuộc của chú thích thành phần. Vì LoginActivity sẽ được Dagger chèn vào, hãy thêm phương thức inject() vào giao diện.

Khi tạo LoginComponent, một bản sao của AppComponent cần phải được truyền vào. Sử dụng nhà máy thành phần để thực hiện việc này:

Kotlin

@Component(dependencies = [AppComponent::class])
interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        fun create(appComponent: AppComponent): LoginComponent
    }

    fun inject(activity: LoginActivity)
}

Java

@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        LoginComponent create(AppComponent appComponent);
    }

    void inject(LoginActivity loginActivity);
}

LoginActivity hiện có thể tạo một bản sao của LoginComponent và gọi phương thức inject().

Kotlin

class LoginActivity: Activity() {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets appComponent from MyApplication available in the base Gradle module
        val appComponent = (applicationContext as MyApplication).appComponent

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this)

        super.onCreate(savedInstanceState)

        // Now you can access loginViewModel
    }
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Gets appComponent from MyApplication available in the base Gradle module
        AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent;

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this);

        // Now you can access loginViewModel
    }
}

LoginViewModel phụ thuộc vào UserRepository; và để LoginComponent có thể truy cập vào từ AppComponent, AppComponent cần hiển thị trong giao diện của nó:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

Quy tắc xác định phạm vi với các thành phần phụ thuộc hoạt động theo cách tương tự như với các thành phần phụ. Vì LoginComponent sử dụng bản sao của AppComponent, nên chúng không thể sử dụng cùng một chú thích phạm vi.

Nếu muốn chuyển phạm vi LoginViewModel sang LoginComponent, bạn cần thực hiện như trước đây bằng cách sử dụng chú thích @ActivityScope tuỳ chỉnh.

Kotlin

@ActivityScope
@Component(dependencies = [AppComponent::class])
interface LoginComponent { ... }

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Các phương pháp hay nhất

  • ApplicationComponent phải luôn nằm trong mô-đun app.

  • Tạo thành phần Dagger trong các mô-đun nếu bạn cần thực hiện thao tác chèn trường trong mô-đun đó, hoặc bạn cần xác định phạm vi các đối tượng cho một luồng cụ thể của ứng dụng.

  • Đối với các mô-đun Gradle là tiện ích hoặc trình trợ giúp và không cần tạo biểu đồ (đó là lý do bạn cần một thành phần Dagger), hãy tạo và hiển thị các mô-đun Dagger công khai với phương thức @Provides và @Binds của những lớp không hỗ trợ chèn hàm khởi tạo.

  • Để sử dụng Dagger trong ứng dụng Android với các mô-đun tính năng, hãy sử dụng các phần phụ thuộc của thành phần để có thể truy cập vào các phần phụ thuộc do ApplicationComponent cung cấp đã xác định trong mô-đun app.