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.
- Sử dụng
- 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
và@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.
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ó
LoginActivity
và RegistrationActivity
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ụ như:
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:
Đ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:
Bản sao của
LoginViewModel
sẽ tồn tại trong bộ nhớ sau khi hoàn tất luồng.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ủaLoginViewModel
, 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ừ ApplicationComponent
vì LoginViewModel
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:
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ínhsubcomponents
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 { }
Thêm mô-đun mới (ở đây là
SubcomponentsModule
) vàoApplicationComponent
: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ènLoginActivity
nữa vì trách nhiệm đó giờ đây thuộc vềLoginComponent
, vì vậy bạn có thể xoá phương thứcinject()
khỏiApplicationComponent
.Người dùng
ApplicationComponent
cần biết cách tạo các bản sao củaLoginComponent
. 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ẹ: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ả ApplicationComponent
và LoginComponent
. 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
.
Vì 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ả LoginComponent
và LoginViewModel
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ư RegistrationComponent
và SettingsComponent
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
, LoginRetrofitService
và LoginViewModel
) 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.
Hãy phân tích các phần của biểu đồ:
NetworkModule
(và do đóLoginRetrofitService
) được đưa vàoApplicationComponent
vì bạn đã chỉ định nó trong thành phần.UserRepository
vẫn ở trongApplicationComponent
vì nó nằm trong phạm vi củaApplicationComponent
. 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ý).Vì
UserRepository
là một phần củaApplicationComponent
, nên các phần phụ thuộc (tức làUserLocalDataSource
vàUserRemoteDataSource
) 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ủaUserRepository
.LoginViewModel
được đưa vàoLoginComponent
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àoApplicationComponent
vì không có phần phụ thuộc nào trongApplicationComponent
cầnLoginViewModel
.Tương tự, nếu trước đó bạn chưa đưa phạm vi
UserRepository
vàoApplicationComponent
, thì Dagger sẽ tự động bao gồmUserRepository
và các phần phụ thuộc của nó như một phần củaLoginComponent
, vì đó hiện là nơi duy nhất sử dụngUserRepository
.
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ònLoginActivity
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 Module1
và Module2
, 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:
- Tái cấu trúc các mô-đun và trích xuất mô-đun chung sang thành phần.
- 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 Module1
và Module2
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à
ModuleXWithModule1Dependencies
và ModuleXWithModule2Dependencies
đượ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.