Tổng quan về ViewModel  Thuộc Android Jetpack.

Lớp ViewModel là một logic kinh doanh hoặc phần tử giữ trạng thái cấp màn hình. Lớp này hiển thị trạng thái cho giao diện người dùng và đóng gói logic kinh doanh liên quan. Ưu điểm chính của lớp này là khả năng lưu trạng thái vào bộ nhớ đệm và duy trì trạng thái đó khi có các thay đổi về cấu hình. Điều này có nghĩa là giao diện người dùng không phải tìm nạp lại dữ liệu khi di chuyển giữa các hoạt động hoặc áp dụng các thay đổi về cấu hình, chẳng hạn như khi xoay màn hình.

Để biết thêm thông tin về phần tử giữ trạng thái, hãy xem hướng dẫn về phần tử giữ trạng thái. Tương tự, để biết thêm thông tin về lớp giao diện người dùng nói chung, hãy xem hướng dẫn về lớp giao diện người dùng.

Lợi ích của ViewModel

Lựa chọn thay thế cho ViewModel là một lớp thuần tuý lưu giữ dữ liệu mà bạn hiển thị trong giao diện người dùng. Điều này có thể trở thành vấn đề khi di chuyển giữa các hoạt động hoặc đích đến Điều hướng (Navigation). Thao tác này sẽ huỷ dữ liệu đó nếu bạn không lưu trữ dữ liệu bằng cơ chế lưu trạng thái của thực thể. ViewModel cung cấp một API thuận tiện để lưu trữ lâu dài dữ liệu giúp giải quyết vấn đề này.

Về cơ bản, lớp ViewModel mang lại 2 lợi ích chính:

  • Lớp này cho phép bạn duy trì trạng thái giao diện người dùng.
  • Lớp này cung cấp quyền truy cập vào logic kinh doanh.

Khả năng lưu trữ dài lâu

ViewModel cho phép lưu trữ dài lâu thông qua cả trạng thái mà ViewModel lưu giữ và các thao tác mà ViewModel kích hoạt. Việc lưu vào bộ nhớ đệm này có nghĩa là bạn không phải tìm nạp lại dữ liệu thông qua các thay đổi phổ biến về cấu hình, chẳng hạn như xoay màn hình.

Phạm vi

Khi tạo bản sao của ViewModel, bạn sẽ truyền lớp này vào đối tượng sẽ triển khai giao diện ViewModelStoreOwner. Đó có thể là đích đến Điều hướng, biểu đồ Điều hướng, hoạt động, mảnh hoặc bất kỳ loại nào khác triển khai giao diện. Sau đó, ViewModel được xác định phạm vi là Vòng đời của ViewModelStoreOwner. Lớp này vẫn nằm trong bộ nhớ cho đến khi ViewModelStoreOwner biến mất vĩnh viễn.

Một loạt các lớp có thể là lớp con trực tiếp hoặc gián tiếp của giao diện ViewModelStoreOwner. Các lớp con trực tiếp là ComponentActivity, FragmentNavBackStackEntry. Để biết danh sách đầy đủ các lớp con gián tiếp, hãy xem tài liệu tham khảo về ViewModelStoreOwner.

Khi mảnh hoặc hoạt động chứa ViewModel bị huỷ bỏ, công việc không đồng bộ sẽ vẫn nằm trong ViewModel thuộc phạm vi mảnh/hoạt động đó. Đây là yếu tố then chốt mang đến khả năng lưu trữ dài lâu.

Để biết thêm thông tin, hãy xem phần dưới đây về Vòng đời ViewModel.

SavedStateHandle

SaveStateHandle cho phép bạn lưu giữ dữ liệu không chỉ khi có các thay đổi về cấu hình mà còn cả khi tái tạo quy trình. Điều này cho phép bạn giữ nguyên trạng thái giao diện người dùng ngay cả khi người dùng đóng và mở ứng dụng vào lúc khác.

Quyền truy cập vào logic kinh doanh

Mặc dù phần lớn logic kinh doanh có trong lớp dữ liệu, nhưng lớp giao diện người dùng cũng có thể chứa logic kinh doanh. Điều này có thể xảy ra khi kết hợp dữ liệu từ nhiều kho lưu trữ để tạo trạng thái giao diện người dùng trên màn hình hoặc khi một loại dữ liệu cụ thể không yêu cầu lớp dữ liệu.

ViewModel là nơi phù hợp để xử lý logic kinh doanh trong lớp giao diện người dùng. ViewModel cũng chịu trách nhiệm xử lý sự kiện và uỷ quyền sự kiện đó cho các lớp khác trong hệ phân cấp khi cần áp dụng logic nghiệp vụ để sửa đổi dữ liệu của ứng dụng.

Jetpack Compose

Khi sử dụng Jetpack Compose, ViewModel là phương tiện chính để hiển thị trạng thái giao diện người dùng trên màn hình cho các thành phần kết hợp. Trong một ứng dụng kết hợp, hoạt động và mảnh chỉ lưu trữ các hàm có khả năng kết hợp. Đây là một sự thay đổi so với các phương pháp trước đây, khi mà việc tạo các phần giao diện người dùng (có các hoạt động và mảnh) có thể tái sử dụng là không hề đơn giản và trực quan, biến chúng trở thành những bộ điều khiển giao diện người dùng phải hoạt động tích cực hơn nhiều.

Điều quan trọng nhất cần lưu ý khi sử dụng ViewModel với Compose là bạn không thể giới hạn ViewModel ở một thành phần kết hợp. Điều này là do một thành phần kết hợp không phải là ViewModelStoreOwner. 2 thực thể của cùng một thành phần kết hợp trong Cấu trúc (Composition) hoặc 2 thành phần kết hợp khác nhau truy cập vào cùng một loại ViewModel trong cùng một ViewModelStoreOwner sẽ nhận được cùng một thực thể của ViewModel. Đây thường là hành vi không mong muốn.

Để nhận được các lợi ích của ViewModel trong Compose, hãy lưu trữ từng màn hình trong một Mảnh/Hoạt động, hoặc sử dụng thành phần Điều hướng trong Compose và dùng lớp ViewModel trong các hàm có khả năng kết hợp càng gần với đích đến Điều hướng càng tốt. Lý do là vì bạn có thể giới hạn ViewModel ở đích đến Điều hướng, biểu đồ Điều hướng, Hoạt động và Mảnh.

Để biết thêm thông tin, hãy xem hướng dẫn về chuyển trạng thái lên trên (state hoisting) cho Jetpack Compose.

Triển khai một ViewModel

Sau đây là ví dụ về cách triển khai ViewModel cho một màn hình cho phép người dùng đổ xúc xắc.

Kotlin

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

Sau đó, bạn có thể truy cập ViewModel trong một hoạt động như sau:

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

Sử dụng coroutine với ViewModel

ViewModel có hỗ trợ cho coroutine Kotlin. Biến này có thể duy trì công việc không đồng bộ theo cách tương tự như khi duy trì trạng thái giao diện người dùng.

Để biết thêm thông tin, hãy xem bài viết Sử dụng coroutine Kotlin với Bộ thành phần cấu trúc Android.

Vòng đời của ViewModel

Vòng đời của ViewModel gắn liền với phạm vi của nó. ViewModel vẫn nằm trong bộ nhớ cho đến khi ViewModelStoreOwner chứa lớp này biến mất. Điều này có thể xảy ra trong các ngữ cảnh sau:

  • Trong trường hợp của một hoạt động, đó là khi hoạt động kết thúc.
  • Trong trường hợp của một mảnh, đó là khi mảnh tách ra.
  • Trong trường hợp một mục nhập Điều hướng, đó là khi mục bị xoá khỏi ngăn xếp lui.

Điều này khiến ViewModel trở thành một giải pháp tuyệt vời để lưu trữ dữ liệu vẫn tồn tại sau khi thay đổi cấu hình.

Hình 1 minh hoạ các trạng thái vòng đời của một hoạt động khi hoạt động đó xoay vòng và sau đó kết thúc. Hình minh hoạ cũng cho thấy thời gian tồn tại của ViewModel bên cạnh vòng đời hoạt động được liên kết. Sơ đồ cụ thể này minh hoạ các trạng thái của một hoạt động. Các trạng thái cơ bản tương tự áp dụng cho vòng đời của một mảnh.

Hình minh hoạ vòng đời của một ViewModel khi một hoạt động thay đổi trạng thái.

Bạn thường yêu cầu một ViewModel trong lần đầu tiên hệ thống gọi phương thức onCreate() của đối tượng hoạt động. Hệ thống có thể gọi onCreate() nhiều lần trong suốt thời gian tồn tại của một hoạt động, chẳng hạn như khi bạn xoay màn hình thiết bị. ViewModel bắt đầu tồn tại từ lần đầu khi bạn yêu cầu một ViewModel cho đến khi hoạt động kết thúc và bị huỷ bỏ.

Xoá các phần phụ thuộc ViewModel

ViewModel gọi phương thức onCleared khi ViewModelStoreOwner huỷ bỏ phương thức đó trong vòng đời. Nhờ vậy, bạn có thể dọn dẹp mọi công việc hoặc phần phụ thuộc theo vòng đời của ViewModel.

Dưới đây là ví dụ minh hoạ một giải pháp thay thế cho viewModelScope. viewModelScope là một CoroutineScope tích hợp sẵn tự động theo vòng đời của ViewModel. ViewModel sử dụng giá trị này để kích hoạt các hoạt động liên quan đến kinh doanh. Nếu bạn muốn sử dụng một phạm vi tuỳ chỉnh thay vì viewModelScope để kiểm thử dễ dàng hơn, thì ViewModel có thể nhận được CoroutineScope dưới dạng phần phụ thuộc trong hàm khởi tạo. Khi ViewModelStoreOwner xoá ViewModel vào lúc kết thúc vòng đời, ViewModel cũng sẽ huỷ CoroutineScope.

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

Từ phiên bản 2.5 trở lên của vòng đời, bạn có thể truyền một hoặc nhiều đối tượng Closeable đến hàm khởi tạo của ViewModel. Hàm này sẽ tự động đóng khi thực thể ViewModel bị xoá.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

Các phương pháp hay nhất

Sau đây là một số phương pháp hay nhất quan trọng mà bạn nên áp dụng khi triển khai ViewModel:

  • Do phạm vi của ViewModel, hãy sử dụng lớp này làm chi tiết triển khai của phần tử giữ trạng thái cấp màn hình. Không dùng các API này làm phần tử giữ trạng thái của các thành phần giao diện người dùng có thể tái sử dụng, chẳng hạn như nhóm khối hoặc biểu mẫu khối. Nếu không, bạn sẽ nhận được cùng một thực thể ViewModel trong nhiều trường hợp sử dụng của cùng một thành phần giao diện người dùng trong cùng một ViewModelStoreOwner.
  • ViewModel không nên biết về chi tiết triển khai giao diện người dùng. Hãy đặt tên càng chung chung càng tốt cho các phương thức mà API ViewModel hiển thị và các trường trạng thái giao diện người dùng. Bằng cách này, ViewModel có thể phù hợp với mọi loại giao diện người dùng: điện thoại di động, thiết bị có thể gập lại, máy tính bảng hoặc thậm chí là Chromebook!
  • Vì ViewModel có thể tồn tại lâu hơn ViewModelStoreOwner, nên lớp này không được chứa bất kỳ tệp tham chiếu nào của các API liên quan đến vòng đời (chẳng hạn như Context hoặc Resources) để ngăn chặn việc rò rỉ bộ nhớ.
  • Không truyền ViewModel cho các lớp, hàm hoặc thành phần giao diện người dùng khác. Vì ViewModel do nền tảng này quản lý, bạn nên giữ kín lớp này. Hãy giữ lớp này gần với Hoạt động, Mảnh hoặc hàm có khả năng kết hợp cấp màn hình của bạn. Điều này giúp ngăn các thành phần cấp thấp hơn truy cập vào dữ liệu và logic nhiều hơn mức cần thiết.

Thông tin khác

Khi dữ liệu của bạn ngày càng phức tạp, bạn có thể chọn một lớp riêng chỉ để tải dữ liệu. Mục đích của ViewModel là đóng gói dữ liệu cho một bộ điều khiển giao diện người dùng nhằm duy trì dữ liệu đó khi có các thay đổi về cấu hình. Để biết thông tin về cách tải, duy trì và quản lý dữ liệu khi có các thay đổi về cấu hình, hãy xem bài viết Lưu trạng thái giao diện người dùng.

Hướng dẫn về Cấu trúc ứng dụng Android đề xuất việc tạo một lớp lưu trữ để xử lý các chức năng này.

Tài nguyên khác

Để biết thêm thông tin về lớp ViewModel, hãy tham khảo các tài nguyên sau.

Tài liệu

Mẫu