Cấu trúc ứng dụng đề xuất của Android khuyến khích việc chia mã thành các lớp để hưởng lợi từ việc tách biệt các vấn đề, một nguyên tắc mà mỗi lớp trong hệ phân cấp chỉ có một trách nhiệm xác định. Điều này dẫn đến việc nhiều lớp nhỏ hơn cần được kết nối với nhau để thực hiện các phần phụ thuộc của nhau.
![Các ứng dụng Android thường có nhiều lớp, và một số lớp trong đó phụ thuộc vào nhau.](https://developer.android.google.cn/static/topic/libraries/architecture/images/final-architecture.png?authuser=19&hl=vi)
Các phần phụ thuộc giữa các lớp có thể được biểu thị 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. Giá trị đại diện của tất cả các lớp và phần phụ thuộc của các lớp đó sẽ tạo nên biểu đồ ứng dụng.
Trong hình 1, bạn có thể thấy bản tóm tắt của biểu đồ ứng dụng.
Khi lớp A (ViewModel
) phụ thuộc vào lớp B (Repository
), có một dòng trỏ từ A đến B đại diện cho phần phụ thuộc đó.
Việc chèn phần phụ thuộc giúp tạo ra các kết nối này đồng thời cho phép bạn hoán đổi các nội dung triển khai để kiểm thử. Chẳng hạn như khi kiểm thử một ViewModel
phụ thuộc vào một kho lưu trữ, bạn có thể truyền các phương thức triển khai khác nhau của
Repository
thông qua phương thức giả mạo hoặc mô phỏng để kiểm thử các trường hợp khác nhau.
Thông tin cơ bản về tính năng chèn phần phụ thuộc theo cách thủ công
Phần này trình bày cách áp dụng tính năng chèn phần phụ thuộc theo cách thủ công trong tình huống thực tế của ứng dụng Android. Phần này sẽ hướng dẫn phương pháp tiếp cận lặp lại về cách bạn có thể bắt đầu sử dụng tính năng chèn phần phụ thuộc trong ứng dụng. Phương pháp này sẽ cải thiện cho đến khi đạt đến điểm rất giống với nội dung mà Dagger sẽ tự động tạo cho bạn. Để biết thêm thông tin về Dagger, vui lòng đọc bài viết Kiến thức cơ bản về Dagger.
Hãy xem luồng là một nhóm màn hình trong ứng dụng tương ứng với một tính năng. Các ví dụ về luồng có thể kể đến là thông tin đăng nhập, đăng ký và quy trình thanh toán.
Khi bao gồm quy trình đăng nhập cho một ứng dụng Android thông thường, LoginActivity
sẽ phụ thuộc vào LoginViewModel
, phụ thuộc vào UserRepository
.
Sau đó, UserRepository
phụ thuộc vào UserLocalDataSource
và
UserRemoteDataSource
phụ thuộc vào Retrofit
.
![](https://developer.android.google.cn/static/images/training/dependency-injection/2-application-graph.png?authuser=19&hl=vi)
LoginActivity
là điểm truy cập vào luồng đăng nhập và người dùng tương tác với hoạt động. Do đó, LoginActivity
cần tạo
LoginViewModel
với tất cả các phần phụ thuộc tương ứng.
Các lớp Repository
và DataSource
của luồng sẽ có dạng như sau:
Kotlin
class UserRepository( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource { ... } class UserRemoteDataSource( private val loginService: LoginRetrofitService ) { ... }
Java
class UserLocalDataSource { public UserLocalDataSource() { } ... } class UserRemoteDataSource { private final Retrofit retrofit; public UserRemoteDataSource(Retrofit retrofit) { this.retrofit = retrofit; } ... } class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } ... }
LoginActivity
sẽ có dạng như sau:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) // Then, satisfy the dependencies of UserRepository val remoteDataSource = UserRemoteDataSource(retrofit) val localDataSource = UserLocalDataSource() // Now you can create an instance of UserRepository that LoginViewModel needs val userRepository = UserRepository(localDataSource, remoteDataSource) // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = LoginViewModel(userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); // Then, satisfy the dependencies of UserRepository UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); UserLocalDataSource localDataSource = new UserLocalDataSource(); // Now you can create an instance of UserRepository that LoginViewModel needs UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = new LoginViewModel(userRepository); } }
Có một vài vấn đề với phương pháp này:
Có rất nhiều mã nguyên mẫu. Nếu bạn muốn tạo bản sao khác của
LoginViewModel
trong một phần khác của mã, bạn có thể gặp phải tình trạng trùng lặp mã.Các phần phụ thuộc phải được khai báo theo thứ tự. Bạn phải tạo bản sao
UserRepository
trướcLoginViewModel
để tạo lớp này.Khó sử dụng lại được đối tượng. Nếu muốn sử dụng lại
UserRepository
trong nhiều tính năng, bạn phải làm cho lớp này tuân theo mẫu singleton. Mẫu singleton khiến việc kiểm thử trở nên khó khăn hơn vì tất cả các kiểm thử đều có cùng một bản sao singleton.
Quản lý các phần phụ thuộc bằng vùng chứa
Để giải quyết vấn đề sử dụng lại các đối tượng, bạn có thể tạo lớp vùng chứa phần phụ thuộc của riêng mình mà bạn dùng để nhận các phần phụ thuộc. Tất cả các bản sao do vùng chứa này cung cấp đều có thể công khai. Ở ví dụ này, vì bạn chỉ cần một bản sao của UserRepository
, nên bạn có thể đặt các phần phụ thuộc của bản sao ở chế độ riêng tư với tuỳ chọn công khai các phần phụ thuộc đó trong tương lai nếu cần cung cấp:
Kotlin
// Container of objects shared across the whole app class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) private val remoteDataSource = UserRemoteDataSource(retrofit) private val localDataSource = UserLocalDataSource() // userRepository is not private; it'll be exposed val userRepository = UserRepository(localDataSource, remoteDataSource) }
Java
// Container of objects shared across the whole app public class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); private UserLocalDataSource localDataSource = new UserLocalDataSource(); // userRepository is not private; it'll be exposed public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); }
Vì các phần phụ thuộc này được sử dụng trên toàn bộ ứng dụng, nên bạn cần đưa các phần phụ thuộc này vào một vị trí chung mà mọi hoạt động đều có thể sử dụng: lớp Application
. Tạo một lớp Application
tuỳ chỉnh chứa bản sao AppContainer
.
Kotlin
// Custom Application class that needs to be specified // in the AndroidManifest.xml file class MyApplication : Application() { // Instance of AppContainer that will be used by all the Activities of the app val appContainer = AppContainer() }
Java
// Custom Application class that needs to be specified // in the AndroidManifest.xml file public class MyApplication extends Application { // Instance of AppContainer that will be used by all the Activities of the app public AppContainer appContainer = new AppContainer(); }
Giờ thì bạn đã có thể lấy bản sao của AppContainer
từ ứng dụng và nhận bản chia sẻ của UserRepository
:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets userRepository from the instance of AppContainer in Application val appContainer = (application as MyApplication).appContainer loginViewModel = LoginViewModel(appContainer.userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets userRepository from the instance of AppContainer in Application AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = new LoginViewModel(appContainer.userRepository); } }
Theo cách này, bạn không sở hữu một singleton UserRepository
. Thay vào đó, bạn có một AppContainer
được chia sẻ trên tất cả hoạt động chứa các đối tượng từ biểu đồ và tạo bản sao các đối tượng mà các lớp khác có thể sử dụng.
Nếu cần LoginViewModel
ở nhiều nơi hơn trong ứng dụng, thì việc sở hữu một vị trí tập trung để bạn tạo các bản sao của LoginViewModel
sẽ rất hợp lý. Bạn có thể di chuyển việc tạo LoginViewModel
vào vùng chứa, đồng thời cung cấp các đối tượng mới thuộc loại đó cho nhà máy (factory). Mã cho một LoginViewModelFactory
sẽ có dạng như sau:
Kotlin
// Definition of a Factory interface with a function to create objects of a type interface Factory<T> { fun create(): T } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory(private val userRepository: UserRepository) : Factory{ override fun create(): LoginViewModel { return LoginViewModel(userRepository) } }
Java
// Definition of a Factory interface with a function to create objects of a type public interface Factory<T> { T create(); } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory implements Factory{ private final UserRepository userRepository; public LoginViewModelFactory(UserRepository userRepository) { this.userRepository = userRepository; } @Override public LoginViewModel create() { return new LoginViewModel(userRepository); } }
Bạn có thể đưa LoginViewModelFactory
vào AppContainer
và làm cho LoginActivity
sử dụng nó:
Kotlin
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) val loginViewModelFactory = LoginViewModelFactory(userRepository) } class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance val appContainer = (application as MyApplication).appContainer loginViewModel = appContainer.loginViewModelFactory.create() } }
Java
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository); } public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = appContainer.loginViewModelFactory.create(); } }
Phương pháp này ổn hơn phương pháp trước, nhưng vẫn còn một số thách thức cần xem xét:
Bạn phải tự quản lý
AppContainer
, tạo bản sao cho tất cả các phần phụ thuộc theo cách thủ công.Vẫn còn nhiều mã nguyên mẫu. Bạn cần tạo các nhà máy hoặc tham số theo cách thủ công, tuỳ thuộc vào việc bạn có muốn sử dụng lại một đối tượng hay không.
Quản lý các phần phụ thuộc trong luồng ứng dụng
AppContainer
sẽ trở nên phức tạp khi bạn muốn thêm nhiều chức năng hơn trong dự án. Khi ứng dụng có quy mô lớn dần và bạn bắt đầu giới thiệu nhiều luồng tính năng khác nhau, thậm chí còn có nhiều vấn đề phát sinh hơn nữa:
Khi có nhiều luồng khác nhau, bạn có thể muốn các đối tượng chỉ nằm trong phạm vi của luồng đó. Ví dụ như khi tạo
LoginUserData
(có thể bao gồm tên người dùng và mật khẩu chỉ sử dụng trong luồng đăng nhập), bạn không muốn lưu giữ lại dữ liệu từ luồng đăng nhập cũ của một người dùng khác. Bạn muốn có một phiên bản mới cho mỗi luồng mới. Bạn có thể đạt được điều đó bằng cách tạo các đối tượngFlowContainer
bên trongAppContainer
, như được minh hoạ trong ví dụ về mã tiếp theo.Việc tối ưu hoá biểu đồ ứng dụng và vùng chứa luồng cũng có thể khó khăn. Bạn cần nhớ xoá các bản sao không cần thiết, tuỳ thuộc vào luồng bạn đang dùng.
Hãy tưởng tượng bạn có một luồng đăng nhập bao gồm một hoạt động (LoginActivity
) và nhiều mảnh (LoginUsernameFragment
và LoginPasswordFragment
). Các thành phần hiển thị này muốn:
Truy cập vào cùng một thực thể
LoginUserData
cần được chia sẻ cho đến khi luồng đăng nhập kết thúc.Tạo một thực thể mới của
LoginUserData
khi luồng bắt đầu lại.
Bạn có thể làm được điều đó bằng cách sử dụng vùng chứa luồng đăng nhập. Vùng chứa này cần được tạo khi luồng đăng nhập bắt đầu và xoá khỏi bộ nhớ khi luồng kết thúc.
Hãy thêm LoginContainer
vào ví dụ về mã. Bạn muốn có thể tạo nhiều bản sao của LoginContainer
trong ứng dụng, thế nên hãy biến lớp này trở thành một lớp (thay vì một singleton) với các phần phụ thuộc mà luồng đăng nhập cần từ AppContainer
.
Kotlin
class LoginContainer(val userRepository: UserRepository) { val loginData = LoginUserData() val loginViewModelFactory = LoginViewModelFactory(userRepository) } // AppContainer contains LoginContainer now class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) // LoginContainer will be null when the user is NOT in the login flow var loginContainer: LoginContainer? = null }
Java
// Container with Login-specific dependencies class LoginContainer { private final UserRepository userRepository; public LoginContainer(UserRepository userRepository) { this.userRepository = userRepository; loginViewModelFactory = new LoginViewModelFactory(userRepository); } public LoginUserData loginData = new LoginUserData(); public LoginViewModelFactory loginViewModelFactory; } // AppContainer contains LoginContainer now public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // LoginContainer will be null when the user is NOT in the login flow public LoginContainer loginContainer; }
Khi đã có vùng chứa dành riêng cho một luồng, bạn phải quyết định thời điểm tạo và xoá bản sao của vùng chứa đó. Vì luồng đăng nhập của bạn nằm độc lập trong một hoạt động (LoginActivity
), nên hoạt động này là hoạt động quản lý vòng đời của vùng chứa đó. LoginActivity
có thể tạo bản sao trong onCreate()
và xoá bản sao đó trong onDestroy()
.
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel private lateinit var loginData: LoginUserData private lateinit var appContainer: AppContainer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) appContainer = (application as MyApplication).appContainer // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = LoginContainer(appContainer.userRepository) loginViewModel = appContainer.loginContainer.loginViewModelFactory.create() loginData = appContainer.loginContainer.loginData } override fun onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null super.onDestroy() } }
Java
public class LoginActivity extends Activity { private LoginViewModel loginViewModel; private LoginData loginData; private AppContainer appContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); appContainer = ((MyApplication) getApplication()).appContainer; // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = new LoginContainer(appContainer.userRepository); loginViewModel = appContainer.loginContainer.loginViewModelFactory.create(); loginData = appContainer.loginContainer.loginData; } @Override protected void onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null; super.onDestroy(); } }
Giống như LoginActivity
, các mảnh đăng nhập có thể truy cập vào LoginContainer
từ AppContainer
và sử dụng bản sao LoginUserData
dùng chung.
Vì trong trường hợp này, bạn đang xử lý logic vòng đời của khung hiển thị, nên việc sử dụng tính năng quan sát vòng đời là hợp lý.
Kết luận
Chèn phần phụ thuộc là một kỹ thuật hay để tạo các ứng dụng Android có khả năng mở rộng và kiểm thử được. Sử dụng vùng chứa như một cách để chia sẻ bản sao của lớp trong các phần khác nhau của ứng dụng, đồng thời làm nơi tập trung để tạo bản sao của lớp bằng cách sử dụng các nhà máy.
Khi ứng dụng trở nên lớn hơn, bạn sẽ bắt đầu thấy việc viết nhiều mã nguyên mẫu (chẳng hạn như nhà máy) có thể dễ xảy ra lỗi. Ngoài ra, bạn còn phải tự quản lý phạm vi và vòng đời của các vùng chứa, tối ưu hoá và loại bỏ các vùng chứa không còn cần thiết để giải phóng bộ nhớ. Thực hiện những điều này không chính xác có thể dẫn đến các lỗi nhỏ và tình trạng rò rỉ bộ nhớ trong ứng dụng.
Trong phần Dagger, bạn sẽ tìm hiểu cách sử dụng Dagger để tự động hoá quy trình này và tạo mã tương tự theo cách thủ công mà bạn đã viết.