Dùng Dagger trong các ứng dụng Android

Trang Kiến thức cơ bản về Dagger đã giải thích cách Dagger có thể giúp bạn tự động chèn phần phụ thuộc trong ứng dụng. Với Dagger, bạn không phải viết mã nguyên mẫu lặp lại và dễ mắc lỗi.

Tóm tắt các phương pháp hay nhất

  • Sử dụng tính năng chèn hàm khởi tạo với @Inject để thêm các loại vào biểu đồ Dager bất cứ khi nào có thể. Trường hợp không:
    • Sử dụng @Binds để cho Dagger biết phương thức triển khai mà một giao diện nên có.
    • Sử dụng @Provides để cho Dagger biết cách cung cấp các lớp mà dự án của bạn không sở hữu.
  • Bạn chỉ nên khai báo các mô-đun một lần trong thành phần.
  • Hãy đặt tên cho các chú thích phạm vi tùy thuộc vào toàn bộ thời gian của chú thích được sử dụng. Ví dụ như @ApplicationScope, @LoggedUserScope@ActivityScope.

Thêm phần phụ thuộc

Để sử dụng Dagger trong dự án, hãy thêm các phần phụ thuộc này vào ứng dụng trong tệp build.gradle. Bạn có thể tìm thấy phiên bản mới nhất của Dagger trong dự án GitHub này.

Kotlin

plugins {
  id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}

Java

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

Dagger trong Android

Hãy xem xét một ứng dụng Android mẫu có biểu đồ phần phụ thuộc trong Hình 1.

LoginActivity phụ thuộc vào LoginViewModel, LoginViewModel phụ thuộc vào UserRepository, UserRepository phụ thuộc vào UserLocalDataSource và UserRemoteDataSource, UserLocalDataSource và UserRemoteDataSource phụ thuộc vào Retrofit.

Hình 1. Biểu đồ phần phụ thuộc của mã ví dụ

Trong Android, bạn thường tạo một biểu đồ Dagger nằm trong lớp ứng dụng vì bạn muốn một bản sao của biểu đồ nằm trong bộ nhớ miễn là ứng dụng đang chạy. Theo đó, biểu đồ sẽ được đính kèm vào vòng đời của ứng dụng. Trong một số trường hợp, bạn cũng có thể muốn có sẵn ngữ cảnh ứng dụng trong biểu đồ. Do đó, bạn cũng cần có biểu đồ này trong lớp Application. Một ưu điểm của phương pháp này là biểu đồ có sẵn cho các lớp khung Android khác. Ngoài ra, phương thức này đơn giản hóa việc kiểm thử bằng cách cho phép bạn sử dụng một lớp Application tùy chỉnh trong kiểm thử.

Vì giao diện tạo biểu đồ được chú thích bằng @Component, nên bạn có thể gọi giao diện này là ApplicationComponent hoặc ApplicationGraph. Bạn thường lưu giữ một bản sao của thành phần đó trong lớp Application tùy chỉnh, và gọi thành phần đó mỗi khi cần biểu đồ ứng dụng, như minh họa trong đoạn mã bên dưới:

Kotlin

// Definition of the Application graph
@Component
interface ApplicationComponent { ... }

// appComponent lives in the Application class to share its lifecycle
class MyApplication: Application() {
    // Reference to the application graph that is used across the whole app
    val appComponent = DaggerApplicationComponent.create()
}

Java

// Definition of the Application graph
@Component
public interface ApplicationComponent {
}

// appComponent lives in the Application class to share its lifecycle
public class MyApplication extends Application {

    // Reference to the application graph that is used across the whole app
    ApplicationComponent appComponent = DaggerApplicationComponent.create();
}

Do một số lớp khung Android nhất định như các hoạt động và mảnh là do hệ thống tạo bản sao, nên Dagger không thể tạo các lớp đó cho bạn. Đối với các hoạt động cụ thể, mã khởi động nào cũng cần phải chuyển vào phương thức onCreate(). Điều đó có nghĩa là bạn không thể sử dụng chú thích @Inject trong hàm khởi tạo của lớp (chèn hàm khởi tạo) như trong các ví dụ trước. Thay vào đó, bạn phải sử dụng tính năng chèn trường.

Thay vì tạo các phần phụ thuộc mà một hoạt động yêu cầu trong phương thức onCreate(), bạn muốn Dagger điền các phần phụ thuộc đó cho bạn. Để chèn trường, thay vào đó, bạn sẽ áp dụng chú thích @Inject cho các trường mà bạn muốn lấy từ biểu đồ Dagger.

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

Java

public class LoginActivity extends Activity {

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

Để đơn giản, LoginViewModel không phải là một Thành phần cấu trúc Android ViewModel; đó chỉ là một lớp thông thường hoạt động như một ViewModel. Để biết thêm thông tin về cách chèn các lớp này, hãy xem mã trong phần cách triển khai Android Blueprints Dagger, chính thức, thuộc nhánh dev-dagger.

Một trong những điểm cần cân nhắc đối với Dagger là các trường được chèn vào không thể ở chế độ riêng tư. Chúng cần có khả năng hiển thị tối thiểu ở chế độ riêng tư theo gói như trong mã trước.

Chèn các hoạt động

Dagger cần biết là LoginActivity phải truy cập vào biểu đồ để có thể cung cấp ViewModel mà nó yêu cầu. Trên trang Kiến thức cơ bản về Dagger, bạn đã sử dụng giao diện @Component để lấy đối tượng từ biểu đồ bằng cách hiển thị các hàm có loại dữ liệu trả về nội dung mà bạn muốn nhận được từ biểu đồ. Trong trường hợp này, bạn cần thông báo cho Dagger về một đối tượng (ở đây là LoginActivity) yêu cầu phần phụ thuộc được chèn vào. Do đó, bạn sẽ hiển thị một hàm nhận tham số làm đối tượng yêu cầu chèn.

Kotlin

@Component
interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is requesting.
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is injecting.
    void inject(LoginActivity loginActivity);
}

Hàm này cho Dagger biết LoginActivity muốn truy cập vào biểu đồ và yêu cầu chèn. Dagger cần đáp ứng tất cả các phần phụ thuộc mà LoginActivity yêu cầu (LoginViewModel với các phần phụ thuộc riêng của nó). Nếu có nhiều lớp yêu cầu chèn, bạn phải khai báo cụ thể tất cả các lớp đó trong thành phần với loại chính xác của chúng. Ví dụ như nếu bạn có LoginActivityRegistrationActivity yêu cầu chèn, bạn sẽ có hai phương thức inject() thay vì một phương thức chung cho cả hai trường hợp. Phương thức inject() chung không cho Dagger biết những gì cần được cung cấp. Bạn có thể đặt tên bất kỳ cho các hàm trong giao diện, nhưng việc gọi các hàm này là inject() khi chúng nhận được đối tượng để chèn làm tham số là một quy ước trong Dagger.

Để chèn một đối tượng vào hoạt động, bạn sẽ sử dụng appComponent được xác định trong lớp Application và gọi phương thức inject(), truyền vào một bản sao của hoạt động yêu cầu chèn.

Khi sử dụng các hoạt động, hãy chèn Dagger vào phương thức onCreate() của hoạt động trước khi gọi super.onCreate() để tránh các vấn đề về khôi phục mảnh. Trong giai đoạn khôi phục trong super.onCreate(), một hoạt động sẽ đính kèm các mảnh có thể muốn truy cập vào các liên kết hoạt động.

Khi sử dụng các mảnh, hãy chèn Dagger vào phương thức onAttach() của mảnh. Trong trường hợp đó, bạn có thể thực hiện việc này trước hoặc sau khi gọi super.onAttach().

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        (applicationContext as MyApplication).appComponent.inject(this)
        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

// @Inject tells Dagger how to create instances of LoginViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

public class LoginActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        ((MyApplication) getApplicationContext()).appComponent.inject(this);
        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

public class LoginViewModel {

    private final UserRepository userRepository;

    // @Inject tells Dagger how to create instances of LoginViewModel
    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Hãy cho Dagger biết cách cung cấp các phần phụ thuộc còn lại để tạo biểu đồ:

Kotlin

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor(
    private val loginService: LoginRetrofitService
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

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

public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    private final LoginRetrofitService loginRetrofitService;

    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}

Mô-đun Dagger

Ở ví dụ này, bạn đang sử dụng thư viện nối mạng Retrofit. UserRemoteDataSource có phần phụ thuộc trong LoginRetrofitService. Tuy nhiên, cách tạo bản sao của LoginRetrofitService khác với những gì bạn đã làm cho đến thời điểm này. Đây không phải là một bản sao của lớp mà là kết quả của việc gọi Retrofit.Builder() và truyền các tham số khác nhau để định cấu hình dịch vụ đăng nhập.

Ngoài chú thích @Inject, có một cách khác để cho Dagger biết cách cung cấp một bản sao của lớp: thông tin bên trong các mô-đun Dagger. Mô-đun Dagger là một lớp được chú thích bằng @Module. Tại đó, bạn có thể xác định các phần phụ thuộc bằng chú thích @Provides.

Kotlin

// @Module informs Dagger that this class is a Dagger Module
@Module
class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)
    }
}

Java

// @Module informs Dagger that this class is a Dagger Module
@Module
public class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}

Mô-đun là phương pháp để gói thông tin về cách cung cấp các đối tượng về mặt ngữ nghĩa. Như bạn có thể thấy, bạn đã gọi lớp NetworkModule để nhóm logic cung cấp các đối tượng liên quan đến việc kết nối mạng. Nếu ứng dụng mở rộng, bạn cũng có thể thêm cách cung cấp OkHttpClient tại đây, hoặc cách định cấu hình Gson hoặcMoshi.

Các phần phụ thuộc của phương thức @Provides là các tham số của phương thức đó. Đối với phương thức trước đó, bạn có thể cung cấp LoginRetrofitService mà không cần phần phụ thuộc vì phương thức này không có tham số. Nếu bạn đã khai báo OkHttpClient dưới dạng tham số, Dagger sẽ cần cung cấp một bản sao OkHttpClient từ biểu đồ để đáp ứng các phần phụ thuộc của LoginRetrofitService. Ví dụ:

Kotlin

@Module
class NetworkModule {
    // Hypothetical dependency on LoginRetrofitService
    @Provides
    fun provideLoginRetrofitService(
        okHttpClient: OkHttpClient
    ): LoginRetrofitService { ... }
}

Java

@Module
public class NetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) {
        ...
    }
}

Để biểu đồ Dagger biết về mô-đun này, bạn phải thêm mô-đun đó vào giao diện @Component như sau:

Kotlin

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

Java

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    ...
}

Bạn nên thêm các loại vào biểu đồ Dagger bằng cách sử dụng tính năng chèn hàm khởi tạo (tức là dùng chú thích @Inject trên hàm khởi tạo của lớp). Đôi khi, bạn không thể làm việc này và phải sử dụng các mô-đun Dagger. Một ví dụ là khi bạn muốn Dagger sử dụng kết quả của một phép tính để xác định cách tạo bản sao của đối tượng. Bất cứ khi nào cần cung cấp một bản sao của loại đó, Dagger cũng sẽ chạy mã bên trong phương thức @Provides.

Biểu đồ Dagger trong ví dụ này hiện đang như sau:

Sơ đồ của biểu đồ phần phụ thuộc LoginActivity

Hình 2. Hình minh họa biểu đồ, trong đó Dagger chèn LoginActivity vào

Điểm truy cập vào biểu đồ là LoginActivity. Bởi vì LoginActivity chèn LoginViewModel , Dagger xây dựng một biểu đồ biết cách cung cấp một bản sao của LoginViewModel và định kỳ của các phần phụ thuộc. Dagger biết cách thực hiện việc này nhờ chú thích @Inject trên hàm khởi tạo của các lớp.

Bên trong ApplicationComponent do Dagger tạo ra, có một phương thức loại nhà máy để nhận bản sao của tất cả các lớp mà lớp đó biết cách cung cấp. Ở ví dụ này, Dagger uỷ quyền cho NetworkModule có trong ApplicationComponent để lấy một thực thể của LoginRetrofitService.

Phạm vi trong Dagger

Phạm vi được đề cập trên trang Kiến thức cơ bản về Dagger như là cách để có một bản sao riêng biệt của một loại trong một thành phần. Điều này có nghĩa là đưa một loại vào vòng đời của một thành phần..

Bởi vì bạn có thể muốn sử dụng UserRepository trong các tính năng khác của ứng dụng và có thể không muốn tạo một đối tượng mới mỗi khi cần, bạn có thể chỉ định đối tượng đó làm phiên bản duy nhất cho toàn bộ ứng dụng. Việc này cũng tương tự như đối với LoginRetrofitService, nghĩa là có thể tốn kém khi tạo, và bạn cũng muốn sử dụng lại một bản sao duy nhất của đối tượng đó. Việc tạo một bản sao của UserRemoteDataSource không quá tốn kém, vì vậy, bạn không cần phải đưa nó vào vòng đời của thành phần.

@Singleton là chú thích phạm vi duy nhất đi kèm với gói javax.inject. Bạn có thể sử dụng đối tượng này để chú thích ApplicationComponent và các đối tượng bạn muốn sử dụng lại trên toàn bộ ứng dụng.

Kotlin

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    fun inject(activity: LoginActivity)
}

@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Module
class NetworkModule {
    // Way to scope types inside a Dagger Module
    @Singleton
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService { ... }
}

Java

@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    void inject(LoginActivity loginActivity);
}

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

@Module
public class NetworkModule {

    @Singleton
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() { ... }
}

Hãy cẩn thận để không làm rò rỉ bộ nhớ khi áp dụng phạm vi cho các đối tượng. Miễn là thành phần phạm vi nằm trong bộ nhớ, thì đối tượng được tạo cũng nằm trong bộ nhớ. Vì ApplicationComponent được tạo khi chạy ứng dụng (trong lớp Application), nên nó sẽ bị hủy khi ứng dụng bị hủy. Do đó, bản sao duy nhất của UserRepository luôn tồn tại trong bộ nhớ cho đến khi ứng dụng bị hủy.

Thành phần phụ của Dagger

Nếu luồng đăng nhập (do LoginActivity quản lý) bao gồm nhiều mảnh, thì bạn nên sử dụng lại cùng một phiên bản của LoginViewModel trong tất cả các mảnh. @Singleton không thể chú thích LoginViewModel để sử dụng lại phiên bản này vì những lý do sau:

  1. Bản sao của LoginViewModel sẽ tồn tại trong bộ nhớ sau khi hoàn tất luồng.

  2. Bạn muốn có một phiên bản LoginViewModel khác cho mỗi luồng đăng nhập. Ví dụ như nếu người dùng đăng xuất, bạn muốn một bản sao khác của LoginViewModel, thay vì cùng một bản sao như khi người dùng đăng nhập lần đầu tiên.

Để đặt phạm vi LoginViewModel vào vòng đời của LoginActivity, bạn cần tạo một thành phần mới (một biểu đồ con mới) cho luồng đăng nhập và phạm vi mới.

Hãy tạo một biểu đồ dành riêng cho luồng đăng nhập.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

LoginActivity lúc này sẽ nhận được lệnh chèn từ LoginComponent vì nó có cấu hình dành riêng cho việc đăng nhập. Thao tác này sẽ loại bỏ trách nhiệm chèn LoginActivity từ lớp ApplicationComponent.

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent phải có quyền truy cập vào các đối tượng từ ApplicationComponentLoginViewModel phụ thuộc vào UserRepository. Bạn có thể sử dụng các thành phần phụ của Dagger để báo cho Dagger biết là bạn muốn một thành phần mới sử dụng một phần của thành phần khác. Thành phần mới phải là thành phần phụ của thành phần chứa tài nguyên dùng chung.

Thành phần phụ là các thành phần kế thừa và mở rộng biểu đồ đối tượng của thành phần mẹ. Do đó, tất cả các đối tượng được cung cấp trong thành phần mẹ cũng sẽ được cung cấp trong thành phần phụ. Theo đó, một đối tượng từ thành phần phụ có thể phụ thuộc vào một đối tượng do thành phần mẹ cung cấp.

Để tạo các bản sao của thành phần phụ, bạn cần có một bản sao của thành phần mẹ. Do đó, các đối tượng do thành phần mẹ cung cấp cho thành phần phụ vẫn thuộc phạm vi của các thành phần mẹ.

Ở ví dụ này, bạn phải xác định LoginComponent là một thành phần phụ của ApplicationComponent. Để thực hiện việc này, hãy chú thích LoginComponent bằng @Subcomponent:

Kotlin

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    fun inject(loginActivity: LoginActivity)
}

Java

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
public interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    void inject(LoginActivity loginActivity);
}

Bạn cũng phải xác định nhà máy thành phần phụ bên trong LoginComponent để ApplicationComponent biết cách tạo các bản sao của LoginComponent.

Kotlin

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

Java

@Subcomponent
public interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject(LoginActivity loginActivity);
}

Để cho Dagger biết LoginComponent là một thành phần phụ của ApplicationComponent, bạn phải chỉ báo bằng cách:

  1. Tạo một mô-đun Dagger mới (ví dụ như SubcomponentsModule) truyền lớp của thành phần phụ đến thuộc tính subcomponents của chú thích.

    Kotlin

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent::class)
    class SubcomponentsModule {}
    

    Java

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent.class)
    public class SubcomponentsModule {
    }
    
  2. Thêm mô-đun mới (ở đây là SubcomponentsModule) vào ApplicationComponent:

    Kotlin

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    }
    

    Java

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = {NetworkModule.class, SubcomponentsModule.class})
    public interface ApplicationComponent {
    }
    

    Vui lòng lưu ý ApplicationComponent không cần chèn LoginActivity nữa vì trách nhiệm đó giờ đây thuộc về LoginComponent, vì vậy bạn có thể xoá phương thức inject() khỏi ApplicationComponent.

    Người dùng ApplicationComponent cần biết cách tạo các bản sao của LoginComponent. Thành phần mẹ phải thêm một phương thức trong giao diện để cho phép người dùng tạo các bản sao của thành phần phụ từ một bản sao của thành phần mẹ:

  3. Hiển thị nhà máy tạo các bản sao của LoginComponent trong giao diện:

    Kotlin

    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    fun loginComponent(): LoginComponent.Factory
    }
    

    Java

    @Singleton
    @Component(modules = { NetworkModule.class, SubcomponentsModule.class} )
    public interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    LoginComponent.Factory loginComponent();
    }
    

Chỉ định phạm vi cho các thành phần phụ

Nếu tạo dự án, bạn có thể tạo các bản sao của cả ApplicationComponentLoginComponent. ApplicationComponent được đính kèm vào vòng đời của ứng dụng vì bạn muốn sử dụng cùng một bản sao của biểu đồ, miễn là ứng dụng đó còn trong bộ nhớ.

Vòng đời của LoginComponent là gì? Một trong những lý do bạn cần LoginComponent là vì bạn cần chia sẻ cùng một bản sao của LoginViewModel giữa các mảnh liên quan đến hoạt động đăng nhập. Ngoài ra, bạn còn muốn có các phiên bản LoginViewModel khác nhau bất cứ khi nào có luồng đăng nhập mới. LoginActivity là vòng đời phù hợp cho LoginComponent: đối với mọi hoạt động mới, bạn cần có một bản sao mới của LoginComponent và các mảnh có thể sử dụng bản sao đó của LoginComponent.

LoginComponent được đính kèm vào vòng đời của LoginActivity, bạn phải giữ lại tệp tham chiếu đến thành phần trong hoạt động tương tự như cách bạn giữ lại tệp tham chiếu đến applicationComponent trong lớp Application. Nhờ đó, các mảnh có thể truy cập vào tệp.

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    ...
}

Vui lòng lưu ý biến loginComponent không được chú thích bằng @Inject vì bạn không mong đợi biến đó được Dagger cung cấp.

Bạn có thể sử dụng ApplicationComponent để tham chiếu đến LoginComponent, sau đó chèn LoginActivity như sau:

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    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)

        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

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

        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

LoginComponent được tạo trong phương thức onCreate() của hoạt động và sẽ bị hủy ngầm khi hoạt động bị hủy.

LoginComponent phải luôn cung cấp cùng một bản sao của LoginViewModel mỗi lần được yêu cầu. Bạn có thể đảm bảo điều này bằng cách tạo phạm vi chú thích tùy chỉnh và chú thích cả LoginComponentLoginViewModel bằng phạm vi đó. Lưu ý là bạn không thể sử dụng chú thích @Singleton, vì chú thích này đã được thành phần mẹ sử dụng, và do đó sẽ khiến đối tượng này trở thành singleton của ứng dụng (một bản sao duy nhất cho toàn bộ ứng dụng). Bạn cần tạo một phạm vi chú thích khác.

Trong trường hợp đó, bạn có thể gọi phạm vi này là @LoginScope, nhưng đây không phải là một phương pháp hay. Tên của chú thích phạm vi không được thể hiện rõ ràng cho mục đích mà nó đáp ứng. Thay vào đó, bạn nên đặt tên theo thời gian tồn tại của chú thích, vì các thành phần đồng cấp chẳng hạn như RegistrationComponentSettingsComponent có thể sử dụng lại chú thích. Đó là lý do bạn nên gọi hàm này là @ActivityScope thay vì @LoginScope.

Kotlin

// Definition of a custom scope called ActivityScope
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// Definition of a custom scope called ActivityScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
public interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

Giờ đây, nếu có hai mảnh cần LoginViewModel, thì cả hai mảnh này đều được cung cấp cùng một phiên bản. Ví dụ như nếu bạn có một LoginUsernameFragment và một LoginPasswordFragment, chúng cần được LoginComponent chèn vào:

Kotlin

@ActivityScope
@Subcomponent
interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    fun inject(loginActivity: LoginActivity)
    fun inject(usernameFragment: LoginUsernameFragment)
    fun inject(passwordFragment: LoginPasswordFragment)
}

Java

@ActivityScope
@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    void inject(LoginActivity loginActivity);
    void inject(LoginUsernameFragment loginUsernameFragment);
    void inject(LoginPasswordFragment loginPasswordFragment);
}

Các thành phần truy cập vào bản sao của thành phần nằm trong đối tượng LoginActivity. Mã mẫu cho LoginUserNameFragment xuất hiện trong đoạn mã sau:

Kotlin

class LoginUsernameFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginUsernameFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Và cũng tương tự cho LoginPasswordFragment:

Kotlin

class LoginPasswordFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginPasswordFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Hình 3 cho thấy biểu đồ Dagger trông như thế nào với thành phần phụ mới. Các lớp có dấu chấm trắng (UserRepository, LoginRetrofitServiceLoginViewModel) là những lớp có một bản sao riêng biệt trong phạm vi các thành phần tương ứng.

Biểu đồ ứng dụng sau khi thêm thành phần phụ cuối cùng

Hình 3. Ví dụ về biểu đồ hiển thị bạn đã tạo cho ứng dụng Android

Hãy phân tích các phần của biểu đồ:

  1. NetworkModule (và do đó LoginRetrofitService) được đưa vào ApplicationComponent vì bạn đã chỉ định nó trong thành phần.

  2. UserRepository vẫn ở trong ApplicationComponent vì nó nằm trong phạm vi của ApplicationComponent. Nếu dự án phát triển, bạn sẽ muốn chia sẻ cùng một phiên bản trên các tính năng khác nhau (ví dụ như tính năng Đăng ký).

    UserRepository là một phần của ApplicationComponent, nên các phần phụ thuộc (tức là UserLocalDataSourceUserRemoteDataSource) cũng cần phải có trong thành phần này để có thể cung cấp các bản sao của UserRepository.

  3. LoginViewModel được đưa vào LoginComponent vì chỉ có các lớp chèn vào từ LoginComponent mới được yêu cầu. LoginViewModel không được đưa vào ApplicationComponent vì không có phần phụ thuộc nào trong ApplicationComponent cần LoginViewModel.

    Tương tự, nếu trước đó bạn chưa đưa phạm vi UserRepository vào ApplicationComponent, thì Dagger sẽ tự động bao gồm UserRepository và các phần phụ thuộc của nó như một phần của LoginComponent, vì đó hiện là nơi duy nhất sử dụng UserRepository.

Ngoài việc xác định phạm vi đối tượng vào một vòng đời khác, bạn nên tạo các thành phần phụ để đóng gói các phần khác của ứng dụng với nhau.

Việc cơ cấu ứng dụng để tạo các đồ thị con Dagger khác nhau tùy thuộc vào luồng ứng dụng, giúpứng dụng hiệu quả hơn và có thể mở rộng về bộ nhớ lẫn thời gian khởi động.

Các phương pháp hay nhất khi xây dựng biểu đồ Dagger

Khi xây dựng biểu đồ Dagger cho ứng dụng của bạn:

  • Khi tạo một thành phần, bạn nên xem xét phần tử nào chịu trách nhiệm cho vòng đời của thành phần đó. Trong trường hợp này, lớp Application chịu trách nhiệm về ApplicationComponent, còn LoginActivity chịu trách nhiệm về LoginComponent.

  • Chỉ sử dụng tính năng xác định phạm vi khi hợp lý. Việc lạm dụng tính năng xác định phạm vi có thể ảnh hưởng tiêu cực đến hiệu suất của ứng dụng trong thời gian chạy: đối tượng nằm trong bộ nhớ miễn là thành phần đó còn nằm trong bộ nhớ, và việc lấy đối tượng theo phạm vi sẽ tốn kém hơn. Khi cung cấp đối tượng, Dagger sử dụng khóa DoubleCheck thay vì trình cung cấp kiểu nhà máy.

Kiểm thử một dự án sử dụng Dagger

Một trong những lợi ích của việc sử dụng các khung chèn phần phụ thuộc như Dagger giúp bạn kiểm thử mã dễ dàng hơn.

Kiểm thử đơn vị

Bạn không cần phải sử dụng Dagger để kiểm thử đơn vị. Khi kiểm thử một lớp sử dụng tính năng chèn hàm khởi tạo, bạn không cần phải sử dụng Dagger để tạo bản sao của lớp đó. Bạn có thể trực tiếp gọi hàm khởi tạo của lớp đó truyền vào các phần phụ thuộc giả mạo hoặc mô phỏng trực tiếp như khi không có chú thích.

Ví dụ như khi kiểm thử LoginViewModel:

Kotlin

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

class LoginViewModelTest {

    @Test
    fun `Happy path`() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        val viewModel = LoginViewModel(fakeUserRepository)
        assertEquals(...)
    }
}

Java

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

public class LoginViewModelTest {

    @Test
    public void happyPath() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        LoginViewModel viewModel = new LoginViewModel(fakeUserRepository);
        assertEquals(...);
    }
}

Kiểm thử toàn diện

Đối với kiểm thử tích hợp, bạn nên tạo một TestApplicationComponent dùng để kiểm thử. Bản chính thức và bản thử nghiệm sử dụng cấu hình thành phần khác nhau.

Bạn phải đầu tư về thiết kế mô-đun hơn trong ứng dụng của mình. Thành phần kiểm thử mở rộng thành phần sản xuất và cài đặt một tập hợp mô-đun khác.

Kotlin

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class])
interface TestApplicationComponent : ApplicationComponent {
}

Java

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// Component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}

FakeNetworkModule có cách triển khai giả mạo NetworkModule ban đầu. Ở đó, bạn có thể cung cấp các bản sao hoặc bản mô phỏng giả mạo bất cứ thứ gì mà bạn muốn thay thế.

Kotlin

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
class FakeNetworkModule {
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        return FakeLoginService()
    }
}

Java

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
public class FakeNetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        return new FakeLoginService();
    }
}

Trong quá trình tích hợp hoặc kiểm thử toàn diện, bạn nên sử dụng TestApplication để tạo TestApplicationComponent thay vì ApplicationComponent.

Kotlin

// Your test application needs an instance of the test graph
class MyTestApplication: MyApplication() {
    override val appComponent = DaggerTestApplicationComponent.create()
}

Java

// Your test application needs an instance of the test graph
public class MyTestApplication extends MyApplication {
    ApplicationComponent appComponent = DaggerTestApplicationComponent.create();
}

Sau đó, ứng dụng kiểm thử này được sử dụng trong một TestRunner tùy chỉnh mà bạn sẽ dùng để chạy kiểm thử đo lường. Để biết thêm thông tin về vấn đề này, vui lòng xem nội dung Sử dụng Dagger trong lớp học lập trình ứng dụng Android.

Làm việc với các mô-đun Dagger

Mô-đun Dagger là một phương pháp đóng gói cách cung cấp đối tượng theo ngữ nghĩa. Bạn không những có thể đưa các mô-đun vào thành phần mà còn có thể đưa mô-đun vào bên trong các mô-đun khác. Đây là một công cụ mạnh mẽ nhưng cũng dễ có thể sử dụng sai cách.

Sau khi mô-đun được thêm vào một thành phần hoặc một mô-đun khác, mô-đun đó đã có trong biểu đồ Dagger; Dagger có thể cung cấp các đối tượng này trong thành phần đó. Trước khi thêm một mô-đun, hãy kiểm tra xem mô-đun đó có trong biểu đồ Dagger hay không bằng cách kiểm tra liệu mô-đun đó đã được thêm vào thành phần hay chưa, hoặc bằng cách biên dịch dự án và xem liệu Dagger có thể tìm thấy các phần phụ thuộc cần thiết cho mô-đun đó hay không.

Bạn chỉ nên khai báo các mô-đun một lần trong một thành phần (ngoài các trường hợp sử dụng Dagger nâng cao cụ thể).

Giả sử bạn đã định cấu hình biểu đồ theo cách này. ApplicationComponent bao gồm Module1Module2, còn Module1 bao gồm ModuleX.

Kotlin

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = {ModuleX.class})
public class Module1 { ... }

@Module
public class Module2 { ... }

Module2 hiện phụ thuộc vào các lớp do ModuleX cung cấp. Bạn không nên đưa ModuleX vào Module2, vì ModuleX sẽ được đưa vào biểu đồ hai lần như trong đoạn mã sau:

Kotlin

// Bad practice: ModuleX is declared multiple times in this Dagger graph
@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module(includes = [ModuleX::class])
class Module2 { ... }

Java

// Bad practice: ModuleX is declared multiple times in this Dagger graph.
@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = ModuleX.class)
public class Module1 { ... }

@Module(includes = ModuleX.class)
public class Module2 { ... }

Thay vào đó, bạn nên thực hiện một trong các thao tác sau:

  1. Tái cấu trúc các mô-đun và trích xuất mô-đun chung sang thành phần.
  2. Tạo một mô-đun mới với các đối tượng mà cả hai mô-đun sẽ chia sẻ và trích xuất mô-đun đó sang thành phần.

Việc không cấu trúc lại theo cách này sẽ dẫn đến tình trạng nhiều mô-đun lẫn lộn mà không có sự rõ ràng về cách sắp xếp, đồng thời khiến bạn khó nhận ra nguồn gốc của từng phần phụ thuộc.

Phương pháp hay (Cách 1): ModuleX được khai báo một lần trong biểu đồ Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleX.class})
public interface ApplicationComponent { ... }

@Module
public class Module1 { ... }

@Module
public class Module2 { ... }

Phương pháp hay (Cách 2): Các phần phụ thuộc chung trên Module1Module2 trong ModuleX được trích xuất sang một mô-đun mới có tên là ModuleXCommon có trong thành phần. Sau đó, hai mô-đun khác có tên là ModuleXWithModule1DependenciesModuleXWithModule2Dependencies được tạo bằng các phần phụ thuộc dành riêng cho từng mô-đun. Tất cả các mô-đun được khai báo một lần trong biểu đồ Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class])
interface ApplicationComponent { ... }

@Module
class ModuleXCommon { ... }

@Module
class ModuleXWithModule1SpecificDependencies { ... }

@Module
class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = [ModuleXWithModule1SpecificDependencies::class])
class Module1 { ... }

@Module(includes = [ModuleXWithModule2SpecificDependencies::class])
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class})
public interface ApplicationComponent { ... }

@Module
public class ModuleXCommon { ... }

@Module
public class ModuleXWithModule1SpecificDependencies { ... }

@Module
public class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = ModuleXWithModule1SpecificDependencies.class)
public class Module1 { ... }

@Module(includes = ModuleXWithModule2SpecificDependencies.class)
public class Module2 { ... }

Chèn hỗ trợ

Chèn hỗ trợ là một mẫu DI dùng để tạo một đối tượng, trong đó một vài tham số có thể được khung DI cung cấp, và một vài tham số khác phải được người dùng chuyển vào tại thời điểm tạo.

Trên Android, mẫu này phổ biến ở các màn hình chi tiết, trong đó mã của phần tử hiển thị chỉ được biết trong thời gian chạy, chứ không phải tại thời điểm biên dịch khi Dagger tạo biểu đồ DI. Để tìm hiểu thêm về tính năng chèn được hỗ trợ với Dagger, vui lòng xem tài liệu về Dagger.

Kết luận

Nếu bạn chưa xem, vui lòng xem lại phần các phương pháp hay nhất. Để biết cách sử dụng Dagger trong một ứng dụng Android, vui lòng xem phần Lớp học lập trình về cách sử dụng Dagger trong ứng dụng Android.