1. Mở đầu
Ở phần trước mình đã giới thiệu một cách tổng quan Dependency Injection là gì. Có nói đến việc implement DI có thể bằng tay (thủ công) hay tự động. Trong bài viết này mình sẽ chia sẻ cách dùng kỹ thuật DI trong một ứng dụng Android bằng cách thủ công cũng như ưu và nhược điểm của nó nhé ^^
2. Application graph
Kiến trúc ứng dụng Android được đề xuất khuyến khích chia mã code của bạn phân tách thành các lớp, modul riêng biệt để hưởng lợi từ separation of concerns, có một nguyên tắc cơ bản đó là mỗi class của hệ thống phân cấp có một trách nhiệm được xác định là duy nhất. Khi mà phân tách như thế thì sẽ dẫn đến nhiều lớp nhỏ hơn cần được kết nối với nhau để thực hiện các dependencies (phụ thuộc) của nhau. Dưới đây là mô hình biểu đồ của ứng dụng Android được google khuyến nghị
Sự phụ thuộc giữa các class có thể đượ biểu diễn dưới dạng biểu đồ, trong đó mỗi lớp được kết nối với các lớp mà nó phụ thuộc vào. Sự thể hiện tất cả các lớp của bạn và các phụ thuộc giữ chúng tạo nên application graph (biểu đồ ứng dụng). Như biểu đồ trên bạn có thể thấy sự trừu tượng của nó. Khi class A (ViewModel) phụ thuộc vào class B (Repository) thì sẽ có một mũi tên từ A đến B chỉ sự phụ thuộc đó.
Dependency injection giúp thực hiện các kết nối này và cho phép chúng ta swap các triển khai cho testing. Ví dụ khi testing ViewModel, class phụ thuộc và Repository, chúng ta có thể dễ dàng test các triển khai của Repository với fakes hoặc mocks để kiếm tra các trường hợp khác nhau.
3. Basic of manual dependency injection
Phần này sẽ hướng dẫn Dependency injection thủ công trong ứng dụng thực Android. Sẽ cho bạn biết cách sử dụng DI trong ứng dụng của mình như thế nào. Các tiếp cận thủ công này sẽ cho bạn hiểu được bản chất của Dagger, nhưng sau này bạn có thể dùng nó một cách tự động.
Xem xét một luồng là một nhóm các màn hình trong ứng dụng phục vụ một tính năng nào đó. Login, Registration, checkout là những ví dụ về luồng.
Một flow đăng nhập của ứng dụng Android thông thường, LoginActivity phụ thuộc vào LoginViewModel, LoginViewModel lại phụ thuộc vào UserRepository. Rồi UserRepository lại phụ thuộc vào UserLocalDataSource và UserRemoteDataSource, UserRemoteDataSource lại phụ thuộc và Retrofit Service, …
LoginActivity là đầu vào của luồng đăng nhập, và là nơi người dùng tương tác. Do đó, LoginActivity cần tạo ra LoginViewModel với tất cả các phụ thuộc của nó.
Các lớp Repository và DataSource sẽ trông như thế này
Kotlin:
1 2 3 4 5 6 7 8 9 10 | <span class="token keyword">class</span> <span class="token function">UserRepository</span><span class="token punctuation">(</span> <span class="token keyword">private</span> <span class="token keyword">val</span> localDataSource<span class="token operator">:</span> UserLocalDataSource<span class="token punctuation">,</span> <span class="token keyword">private</span> <span class="token keyword">val</span> remoteDataSource<span class="token operator">:</span> UserRemoteDataSource <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">..</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> UserLocalDataSource <span class="token punctuation">{</span> <span class="token operator">..</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token function">UserRemoteDataSource</span><span class="token punctuation">(</span> <span class="token keyword">private</span> <span class="token keyword">val</span> loginService<span class="token operator">:</span> LoginRetrofitService <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">..</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <span class="token keyword">class</span> <span class="token class-name">UserLocalDataSource</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token function">UserLocalDataSource</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token class-name">UserRemoteDataSource</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">final</span> Retrofit retrofit<span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token function">UserRemoteDataSource</span><span class="token punctuation">(</span>Retrofit retrofit<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>retrofit <span class="token operator">=</span> retrofit<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token class-name">UserRepository</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">final</span> UserLocalDataSource userLocalDataSource<span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">final</span> UserRemoteDataSource userRemoteDataSource<span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token function">UserRepository</span><span class="token punctuation">(</span>UserLocalDataSource userLocalDataSource<span class="token punctuation">,</span> UserRemoteDataSource userRemoteDataSource<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userLocalDataSource <span class="token operator">=</span> userLocalDataSource<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userRemoteDataSource <span class="token operator">=</span> userRemoteDataSource<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> |
Đây là code trong LoginActivity:
Kotlin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <span class="token keyword">class</span> LoginActivity<span class="token operator">:</span> <span class="token function">Activity</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">lateinit</span> <span class="token keyword">var</span> loginViewModel<span class="token operator">:</span> LoginViewModel <span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token operator">:</span> Bundle<span class="token operator">?</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span> <span class="token comment">// In order to satisfy the dependencies of LoginViewModel, you have to also</span> <span class="token comment">// satisfy the dependencies of all of its dependencies recursively.</span> <span class="token comment">// First, create retrofit which is the dependency of UserRemoteDataSource</span> <span class="token keyword">val</span> retrofit <span class="token operator">=</span> Retrofit<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span><span class="token string">"https://example.com"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>LoginService<span class="token operator">::</span><span class="token keyword">class</span><span class="token punctuation">.</span>java<span class="token punctuation">)</span> <span class="token comment">// Then, satisfy the dependencies of UserRepository</span> <span class="token keyword">val</span> remoteDataSource <span class="token operator">=</span> <span class="token function">UserRemoteDataSource</span><span class="token punctuation">(</span>retrofit<span class="token punctuation">)</span> <span class="token keyword">val</span> localDataSource <span class="token operator">=</span> <span class="token function">UserLocalDataSource</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// Now you can create an instance of UserRepository that LoginViewModel needs</span> <span class="token keyword">val</span> userRepository <span class="token operator">=</span> <span class="token function">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span> <span class="token comment">// Lastly, create an instance of LoginViewModel with userRepository</span> loginViewModel <span class="token operator">=</span> <span class="token function">LoginViewModel</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MainActivity</span> <span class="token keyword">extends</span> <span class="token class-name">Activity</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> LoginViewModel loginViewModel<span class="token punctuation">;</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">onCreate</span><span class="token punctuation">(</span>Bundle savedInstanceState<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setContentView</span><span class="token punctuation">(</span>R<span class="token punctuation">.</span>layout<span class="token punctuation">.</span>activity_main<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// In order to satisfy the dependencies of LoginViewModel, you have to also</span> <span class="token comment">// satisfy the dependencies of all of its dependencies recursively.</span> <span class="token comment">// First, create retrofit which is the dependency of UserRemoteDataSource</span> Retrofit retrofit <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Retrofit<span class="token punctuation">.</span>Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span><span class="token string">"https://example.com"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>LoginService<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Then, satisfy the dependencies of UserRepository</span> UserRemoteDataSource remoteDataSource <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserRemoteDataSource</span><span class="token punctuation">(</span>retrofit<span class="token punctuation">)</span><span class="token punctuation">;</span> UserLocalDataSource localDataSource <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserLocalDataSource</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Now you can create an instance of UserRepository that LoginViewModel needs</span> UserRepository userRepository <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Lastly, create an instance of LoginViewModel with userRepository</span> loginViewModel <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LoginViewModel</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Với cách tiếp cận cũ này sẽ có vấn đề sau:
- Có rất nhiều code được viết ra. Nếu ta muốn tạo ra một LoginViewModel trong một nơi khác ta phải duplicate đoạn code trên.
- Sự phụ thuộc phải được khái báo thứ tự thì mới chạy được. Ta phải tạo Retrofit trước RemoteDataSource, phải tạo các DataSource trước UserRepository, phải tạo UserRepository trước LoginViewModel.
- Thật khó để sử dụng lại các object. Nếu bạn muốn sử dụng lại UserRepository trên nhiều tính năng khác, bạn phải cho nó follow theo singleton pattern. Nhưng singleton pattern làm cho việc testing trở nên khó khăn bởi vì tất cả các trường hợp test chỉ có đối tượng của UserRepository tạo ra.
4. Managing dependency with a container
Để giải quyết vấn đề tái sử dụng object trên, ta có thể tạo ra một lớp dependencies container để dùng chứa các phụ thuộc. Tất cả các thể hiện được cung cấp bởi lớp này có thể để ở trạng thái public. Trong ví dụ trên ta chỉ cần một thể hiện của UserRepository và có thể dùng nhiều nơi. Có thể đặt các dependencies private và khi nào được cung cấp thì chuyển sang public.
Kotlin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token comment">// Container of objects shared across the whole app</span> <span class="token keyword">class</span> AppContainer <span class="token punctuation">{</span> <span class="token comment">// Since you want to expose userRepository out of the container, you need to satisfy</span> <span class="token comment">// its dependencies as you did before</span> <span class="token keyword">private</span> <span class="token keyword">val</span> retrofit <span class="token operator">=</span> Retrofit<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span><span class="token string">"https://example.com"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>LoginService<span class="token operator">::</span><span class="token keyword">class</span><span class="token punctuation">.</span>java<span class="token punctuation">)</span> <span class="token keyword">private</span> <span class="token keyword">val</span> remoteDataSource <span class="token operator">=</span> <span class="token function">UserRemoteDataSource</span><span class="token punctuation">(</span>retrofit<span class="token punctuation">)</span> <span class="token keyword">private</span> <span class="token keyword">val</span> localDataSource <span class="token operator">=</span> <span class="token function">UserLocalDataSource</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// userRepository is not private; it'll be exposed</span> <span class="token keyword">val</span> userRepository <span class="token operator">=</span> <span class="token function">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token comment">// Container of objects shared across the whole app</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">AppContainer</span> <span class="token punctuation">{</span> <span class="token comment">// Since you want to expose userRepository out of the container, you need to satisfy</span> <span class="token comment">// its dependencies as you did before</span> <span class="token keyword">private</span> Retrofit retrofit <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Retrofit<span class="token punctuation">.</span>Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span><span class="token string">"https://example.com"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>LoginService<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">private</span> UserRemoteDataSource remoteDataSource <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserRemoteDataSource</span><span class="token punctuation">(</span>retrofit<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">private</span> UserLocalDataSource localDataSource <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserLocalDataSource</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// userRepository is not private; it'll be exposed</span> <span class="token keyword">public</span> UserRepository userRepository <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Bởi vì những dependencies này được sử dụng trên toàn bộ ứng dụng, chúng cần được đặt ở một nơi chung mà tất cả các Activity có thể sử dụng: Application class. Tạo mộ lớp custom application chứa một thể hiện của AppContainer.
Kotlin:
1 2 3 4 5 6 7 8 | <span class="token comment">// Custom Application class that needs to be specified</span> <span class="token comment">// in the AndroidManifest.xml file</span> <span class="token keyword">class</span> MyApplication <span class="token operator">:</span> <span class="token function">Application</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Instance of AppContainer that will be used by all the Activities of the app</span> <span class="token keyword">val</span> appContainer <span class="token operator">=</span> <span class="token function">AppContainer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 | <span class="token comment">// Custom Application class that needs to be specified</span> <span class="token comment">// in the AndroidManifest.xml file</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MyApplication</span> <span class="token keyword">extends</span> <span class="token class-name">Application</span> <span class="token punctuation">{</span> <span class="token comment">// Instance of AppContainer that will be used by all the Activities of the app</span> <span class="token keyword">public</span> AppContainer appContainer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AppContainer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Chú ý rằng AppContainer chỉ là một class thông thường với một thể hiện duy nhất được chia sẻ và đặt trong Application Class. Và nó không theo singleton pattern (Không phải là object trong Kotlin, và Java, không được truy cập bằng phương thức Singleton.getInstance() điển hình)
Bây giờ ta có thể lấy thể hiện của AppContainer từ ứng dụng và nhận được chia sẻ của các cá thể UserRepository.
Kotlin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">public</span> <span class="token keyword">class</span> MainActivity extends Activity <span class="token punctuation">{</span> <span class="token keyword">private</span> LoginViewModel loginViewModel<span class="token punctuation">;</span> <span class="token annotation builtin">@Override</span> <span class="token keyword">protected</span> void <span class="token function">onCreate</span><span class="token punctuation">(</span>Bundle savedInstanceState<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setContentView</span><span class="token punctuation">(</span>R<span class="token punctuation">.</span>layout<span class="token punctuation">.</span>activity_main<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Gets userRepository from the instance of AppContainer in Application</span> AppContainer appContainer <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>MyApplication<span class="token punctuation">)</span> <span class="token function">getApplication</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>appContainer<span class="token punctuation">;</span> loginViewModel <span class="token operator">=</span> new <span class="token function">LoginViewModel</span><span class="token punctuation">(</span>appContainer<span class="token punctuation">.</span>userRepository<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MainActivity</span> <span class="token keyword">extends</span> <span class="token class-name">Activity</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> LoginViewModel loginViewModel<span class="token punctuation">;</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">onCreate</span><span class="token punctuation">(</span>Bundle savedInstanceState<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setContentView</span><span class="token punctuation">(</span>R<span class="token punctuation">.</span>layout<span class="token punctuation">.</span>activity_main<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Gets userRepository from the instance of AppContainer in Application</span> AppContainer appContainer <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>MyApplication<span class="token punctuation">)</span> <span class="token function">getApplication</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>appContainer<span class="token punctuation">;</span> loginViewModel <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LoginViewModel</span><span class="token punctuation">(</span>appContainer<span class="token punctuation">.</span>userRepository<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Trong ví dụ này bạn không có một Singleton của UserRepository. Thay vào đó bạn có một AppContainer được chia sẻ cho các Activity.
Nếu LoginViewModel là cần thiết ở nhiều nơi trong ứng dụng. Có một nơi tập trung mà bạn tạo ra thể hiện của nó. Ta có thể tạo LoginViewModel trong container và cung cấp các đối tượng mới theo Factory.
Kotlin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <span class="token comment">// Definition of a Factory interface with a function to create objects of a type</span> <span class="token keyword">interface</span> Factory <span class="token punctuation">{</span> <span class="token keyword">fun</span> <span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> T <span class="token punctuation">}</span> <span class="token comment">// Factory for LoginViewModel.</span> <span class="token comment">// Since LoginViewModel depends on UserRepository, in order to create instances of</span> <span class="token comment">// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.</span> <span class="token keyword">class</span> <span class="token function">LoginViewModelFactory</span><span class="token punctuation">(</span><span class="token keyword">private</span> <span class="token keyword">val</span> userRepository<span class="token operator">:</span> UserRepository<span class="token punctuation">)</span> <span class="token operator">:</span> Factory <span class="token punctuation">{</span> <span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> LoginViewModel <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">LoginViewModel</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token comment">// Definition of a Factory interface with a function to create objects of a type</span> <span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">Factory</span> <span class="token punctuation">{</span> T <span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// Factory for LoginViewModel.</span> <span class="token comment">// Since LoginViewModel depends on UserRepository, in order to create instances of</span> <span class="token comment">// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.</span> <span class="token keyword">class</span> <span class="token class-name">LoginViewModelFactory</span> <span class="token keyword">implements</span> <span class="token class-name">Factory</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">final</span> UserRepository userRepository<span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token function">LoginViewModelFactory</span><span class="token punctuation">(</span>UserRepository userRepository<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userRepository <span class="token operator">=</span> userRepository<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> LoginViewModel <span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">LoginViewModel</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Sau đó ta có thể khai báo LoginViewModelFactory trong AppContainer và lấy được LoginViewModel ở LoginActivity.
Kotlin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token comment">// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory</span> <span class="token keyword">class</span> AppContainer <span class="token punctuation">{</span> <span class="token operator">..</span><span class="token punctuation">.</span> <span class="token keyword">val</span> userRepository <span class="token operator">=</span> <span class="token function">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span> <span class="token keyword">val</span> loginViewModelFactory <span class="token operator">=</span> <span class="token function">LoginViewModelFactory</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> LoginActivity<span class="token operator">:</span> <span class="token function">Activity</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">lateinit</span> <span class="token keyword">var</span> loginViewModel<span class="token operator">:</span> LoginViewModel <span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token operator">:</span> Bundle<span class="token operator">?</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span> <span class="token comment">// Gets LoginViewModelFactory from the application instance of AppContainer</span> <span class="token comment">// to create a new LoginViewModel instance</span> <span class="token keyword">val</span> appContainer <span class="token operator">=</span> <span class="token punctuation">(</span>application <span class="token keyword">as</span> MyApplication<span class="token punctuation">)</span><span class="token punctuation">.</span>appContainer loginViewModel <span class="token operator">=</span> appContainer<span class="token punctuation">.</span>loginViewModelFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <span class="token comment">// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">AppContainer</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">public</span> UserRepository userRepository <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">public</span> LoginViewModelFactory loginViewModelFactory <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LoginViewModelFactory</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">MainActivity</span> <span class="token keyword">extends</span> <span class="token class-name">Activity</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> LoginViewModel loginViewModel<span class="token punctuation">;</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">onCreate</span><span class="token punctuation">(</span>Bundle savedInstanceState<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setContentView</span><span class="token punctuation">(</span>R<span class="token punctuation">.</span>layout<span class="token punctuation">.</span>activity_main<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Gets LoginViewModelFactory from the application instance of AppContainer</span> <span class="token comment">// to create a new LoginViewModel instance</span> AppContainer appContainer <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>MyApplication<span class="token punctuation">)</span> <span class="token function">getApplication</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>appContainer<span class="token punctuation">;</span> loginViewModel <span class="token operator">=</span> appContainer<span class="token punctuation">.</span>loginViewModelFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Các tiếp cận này tốt hơn phương pháp trên đó nhưng vẫn còn một số thứ cần xem xét:
- Bạn phải tự quản lý AppContainer, tạo các thể hiện của dependencies bằng tay.
- Vẫn rất còn nhiều code phải viết. Bạn cần tạo ra các factories và các tham số bằng tay tùy thuộc bạn có muôn sử dụng một đối tượng hay không.
5. Managing dependencies in application flows
AppContainer sẽ trở nên cồng kềnh khi ứng dụng có nhiều chức năng. Khi ứng dụng lớn ta lại bắt đầu thực hiện những bước tương tự, thậm chí còn nhiều thứ phát sinh:
- Khi bạn có các luồng tính năng khác nhau, bạn cũng có thể chỉ muốn các đối tượng chỉ sống trong phạm vi vùng đó. Ví dụ trong trường hợp bạn tạo một LoginUserData (gồm username và password) thì bạn không muốn lưu trữ dữ liệu từ luồng đăng nhập cũ từ một người dùng khác. Bạn cần một dữ liệu mới cho luồng mới. Bạn có đạt được điều đó bằng cách tạo ra đối tượng FlowContainer trong AppContainer (ví dụ ở bên dưới)
- Tối ưu hóa Application Graph và flow containers có thể khó khăn.
Chúng ta hãy tạo một LoginContainer. Ta có thể tạo nhiều thế hiện LoginContainer trong ứng dụng nếu muốn, vì vậy thay vì biến nó thành một Singleton hãy biến nó thành một lớp với các phụ thuộc mà luồng đăng nhập cần từ AppContainer
Kotlin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span class="token keyword">class</span> <span class="token function">LoginContainer</span><span class="token punctuation">(</span><span class="token keyword">val</span> userRepository<span class="token operator">:</span> UserRepository<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">val</span> loginData <span class="token operator">=</span> <span class="token function">LoginUserData</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">val</span> loginViewModelFactory <span class="token operator">=</span> <span class="token function">LoginViewModelFactory</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token comment">// AppContainer contains LoginContainer now</span> <span class="token keyword">class</span> AppContainer <span class="token punctuation">{</span> <span class="token operator">..</span><span class="token punctuation">.</span> <span class="token keyword">val</span> userRepository <span class="token operator">=</span> <span class="token function">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span> <span class="token comment">// LoginContainer will be null when the user is NOT in the login flow</span> <span class="token keyword">var</span> loginContainer<span class="token operator">:</span> LoginContainer<span class="token operator">?</span> <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token comment">// Container with Login-specific dependencies</span> <span class="token keyword">class</span> <span class="token class-name">LoginContainer</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">final</span> UserRepository userRepository<span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token function">LoginContainer</span><span class="token punctuation">(</span>UserRepository userRepository<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userRepository <span class="token operator">=</span> userRepository<span class="token punctuation">;</span> loginViewModelFactory <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LoginViewModelFactory</span><span class="token punctuation">(</span>userRepository<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> LoginUserData loginData <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LoginUserData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">public</span> LoginViewModelFactory loginViewModelFactory<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment">// AppContainer contains LoginContainer now</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">AppContainer</span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">public</span> UserRepository userRepository <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UserRepository</span><span class="token punctuation">(</span>localDataSource<span class="token punctuation">,</span> remoteDataSource<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// LoginContainer will be null when the user is NOT in the login flow</span> <span class="token keyword">public</span> LoginContainer loginContainer<span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Khi bạn có một Container cho một Flow, bạn phải quyết định khi nào tạo và xóa thể hiện của Container. Bởi vì luồng đăng nhập khép kín trong ActivityLogin. Do đó ta tạo thể hiện trong onCreate() và xóa nó trong onDestroy()
Kotlin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <span class="token keyword">class</span> LoginActivity<span class="token operator">:</span> <span class="token function">Activity</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">lateinit</span> <span class="token keyword">var</span> loginViewModel<span class="token operator">:</span> LoginViewModel <span class="token keyword">private</span> <span class="token keyword">lateinit</span> <span class="token keyword">var</span> loginData<span class="token operator">:</span> LoginUserData <span class="token keyword">private</span> <span class="token keyword">lateinit</span> <span class="token keyword">var</span> appContainer<span class="token operator">:</span> AppContainer <span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token operator">:</span> Bundle<span class="token operator">?</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span> appContainer <span class="token operator">=</span> <span class="token punctuation">(</span>application <span class="token keyword">as</span> MyApplication<span class="token punctuation">)</span><span class="token punctuation">.</span>appContainer <span class="token comment">// Login flow has started. Populate loginContainer in AppContainer</span> appContainer<span class="token punctuation">.</span>loginContainer <span class="token operator">=</span> <span class="token function">LoginContainer</span><span class="token punctuation">(</span>appContainer<span class="token punctuation">.</span>userRepository<span class="token punctuation">)</span> loginViewModel <span class="token operator">=</span> appContainer<span class="token punctuation">.</span>loginContainer<span class="token punctuation">.</span>loginViewModelFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span> loginData <span class="token operator">=</span> appContainer<span class="token punctuation">.</span>loginContainer<span class="token punctuation">.</span>loginData <span class="token punctuation">}</span> <span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Login flow is finishing</span> <span class="token comment">// Removing the instance of loginContainer in the AppContainer</span> appContainer<span class="token punctuation">.</span>loginContainer <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">LoginActivity</span> <span class="token keyword">extends</span> <span class="token class-name">Activity</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> LoginViewModel loginViewModel<span class="token punctuation">;</span> <span class="token keyword">private</span> LoginData loginData<span class="token punctuation">;</span> <span class="token keyword">private</span> AppContainer appContainer<span class="token punctuation">;</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">onCreate</span><span class="token punctuation">(</span>Bundle savedInstanceState<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span>savedInstanceState<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setContentView</span><span class="token punctuation">(</span>R<span class="token punctuation">.</span>layout<span class="token punctuation">.</span>activity_main<span class="token punctuation">)</span><span class="token punctuation">;</span> appContainer <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>MyApplication<span class="token punctuation">)</span> <span class="token function">getApplication</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>appContainer<span class="token punctuation">;</span> <span class="token comment">// Login flow has started. Populate loginContainer in AppContainer</span> appContainer<span class="token punctuation">.</span>loginContainer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LoginContainer</span><span class="token punctuation">(</span>appContainer<span class="token punctuation">.</span>userRepository<span class="token punctuation">)</span><span class="token punctuation">;</span> loginViewModel <span class="token operator">=</span> appContainer<span class="token punctuation">.</span>loginContainer<span class="token punctuation">.</span>loginViewModelFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> loginData <span class="token operator">=</span> appContainer<span class="token punctuation">.</span>loginContainer<span class="token punctuation">.</span>loginData<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Login flow is finishing</span> <span class="token comment">// Removing the instance of loginContainer in the AppContainer</span> appContainer<span class="token punctuation">.</span>loginContainer <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
6. Kết luận
Dependency injection là một kỹ thuật tốt để tạo ra các ứng dụng Android có thể mở rộng và dễ dàng để testing. Sử dụng các Containers như một cách để chia sẻ các thể hiện của các phần khác nhau trong ứng dụng, hay nói cách khác nó là nơi tập trung để tạo ra các thể hiện của các lớp (using factories)
Khi ứng dụng của bạn lớn hơn, bạn sẽ bắt đầu thấy rằng bạn phải viết rất nhiều code (như factories), do đó có thể dễ bị lỗi. Bạn cũng phải tự mình quản lý phạm vi và vòng đời của các container, tối ưu hóa và loại bỏ các container không cần thiết để giải phóng bộ nhớ. Điều này nếu không làm chính xác có thể dẫn đến những lỗi rất tinh vi hay là memory leaks (rò rỉ bộ nhớ) trong ứng dụng của bạn. Đây chính là nhược điểm của cách dùng Dagger thủ công
Trong phần tới mình sẽ chia sẽ cách dùng Dagger một cách tự động. Những kiến thức chia sẻ này mình tham khảo của Google, cám ơn bạn đã theo dõi bài viết.