Chèn phần phụ thuộc theo cách thủ công

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.
Hình 1. Mô hình biểu đồ ứng dụng của ứng dụng Android

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 UserLocalDataSourceUserRemoteDataSource. Điều này lại phụ thuộc vào dịch vụ Retrofit.

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 RepositoryDataSource 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:

  1. 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ã.

  2. 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ước LoginViewModel để tạo lớp này.

  3. 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:

  1. 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.

  2. 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:

  1. 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ượng FlowContainer bên trong AppContainer, như được minh hoạ trong ví dụ về mã tiếp theo.

  2. 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 (LoginUsernameFragmentLoginPasswordFragment). Các thành phần hiển thị này muốn:

  1. 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.

  2. 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.