Tổng quan về ViewModel   Một phần của Android Jetpack.

Lớp (class) ViewModel được thiết kế nhằm lưu trữ và quản lý dữ liệu liên quan đến giao diện người dùng theo cách nhận biết vòng đời. Lớp ViewModel duy trì dữ liệu sau các thay đổi về cấu hình, chẳng hạn như thao tác xoay màn hình.

Khung Android quản lý vòng đời của các bộ điều khiển giao diện người dùng, chẳng hạn như các hoạt động và mảnh (fragment). Khung này có thể quyết định việc huỷ hoặc tạo lại một bộ điều khiển giao diện người dùng để phản hồi một số thao tác nhất định của người dùng hoặc các sự kiện hoàn toàn nằm ngoài tầm kiểm soát của bạn trên thiết bị.

Nếu hệ thống huỷ hoặc tạo lại bộ điều khiển giao diện người dùng, thì mọi dữ liệu tạm thời liên quan đến giao diện người dùng mà bạn lưu trữ trong đó sẽ bị mất. Ví dụ: ứng dụng của bạn có thể chứa một danh sách những người dùng thuộc một trong những hoạt động của ứng dụng đó. Khi hoạt động được tạo lại để thay đổi cấu hình, hoạt động mới phải tìm nạp lại danh sách người dùng. Đối với dữ liệu đơn giản, hoạt động có thể sử dụng phương thức onSaveInstanceState() và khôi phục dữ liệu trong gói của onCreate(), tuy nhiên phương pháp này chỉ phù hợp với lượng dữ liệu nhỏ có thể được tuần tự hoá và sau đó giải tuần tự hoá, không phù hợp với lượng dữ liệu có thể có kích thước lớn như danh sách người dùng hoặc bitmap.

Một vấn đề khác là những lệnh gọi không đồng bộ do bộ điều khiển giao diện người dùng cần thực hiện thường xuyên có thể mất một chút thời gian để trả về. Bộ điều khiển giao diện người dùng cần quản lý các lệnh gọi này và đảm bảo việc hệ thống dọn sạch chúng sau khi huỷ bộ điều khiển để tránh việc rò rỉ bộ nhớ tiềm ẩn. Việc quản lý này đòi hỏi nhiều thời gian bảo trì. Trong trường hợp đối tượng được tạo lại để thay đổi cấu hình, bạn sẽ lãng phí tài nguyên vì đối tượng có thể phải phát hành lại các lệnh gọi đã thực hiện trước đó.

Các bộ điều khiển giao diện người dùng như các hoạt động và mảnh chủ yếu dùng để hiển thị dữ liệu giao diện người dùng, phản ứng với các thao tác của người dùng hoặc xử lý nội dung giao tiếp của hệ điều hành, chẳng hạn như các yêu cầu cấp quyền. Lớp sẽ bị quá tải nếu bạn yêu cầu bộ điều khiển giao diện người dùng chịu thêm trách nhiệm tải dữ liệu từ cơ sở dữ liệu hoặc mạng. Việc chỉ định quá nhiều trách nhiệm cho bộ điều khiển giao diện người dùng có thể dẫn đến việc một lớp cố gắng tự xử lý tất cả công việc của một ứng dụng, thay vì uỷ quyền cho các lớp khác. Điều này cũng khiến cho việc kiểm thử trở nên khó khăn hơn nhiều.

Sẽ dễ dàng và hiệu quả hơn khi bạn tách biệt chế độ xem quyền sở hữu dữ liệu khỏi logic bộ điều khiển giao diện người dùng.

Triển khai một ViewModel

Bộ thành phần cấu trúc ViewModel cung cấp lớp trợ giúp cho bộ điều khiển giao diện người dùng chịu trách nhiệm chuẩn bị dữ liệu cho giao diện người dùng. Các đối tượng ViewModel tự động được giữ lại khi có thay đổi về cấu hình. Nhờ đó, thực thể tiếp theo của hoạt động hoặc mảnh sẽ có thể sử dụng ngay dữ liệu mà các đối tượng này lưu trữ. Ví dụ: nếu bạn cần hiển thị danh sách người dùng trong ứng dụng, hãy nhớ chỉ định trách nhiệm thu nạp và giữ danh sách người dùng cho một ViewModel, thay vì một hoạt động hoặc mảnh, được minh hoạ bằng mã mẫu sau đây:

Kotlin

class MyViewModel : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }

    fun getUsers(): LiveData<List<User>> {
        return users
    }

    private fun loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

Java

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

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

Kotlin

class MyActivity : 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 MyViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val model: MyViewModel by viewModels()
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle 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.

        MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

Nếu được tạo lại thì hoạt động sẽ nhận được cùng một thực thể MyViewModel do hoạt động đầu tiên tạo ra. Khi hoạt động chủ hoàn tất, khung sẽ gọi phương thức onCleared()của đối tượng ViewModel để phương thức có thể dọn dẹp tài nguyên.

Các đối tượng ViewModel được thiết kế để kéo dài hơn bản sao của các chế độ xem cụ thể hoặc LifecycleOwners. Cách thiết kế này cũng cho phép bạn có thể dễ dàng viết các kiểm thử để kiểm tra một ViewModel vì lớp đó không biết về chế độ xem và các đối tượng Lifecycle. Các đối tượng ViewModel có thể chứa LifecycleObservers, chẳng hạn như các đối tượng LiveData. Tuy nhiên, các đối tượng ViewModel không được phép quan sát những thay đổi của các đối tượng nhận biết vòng đời quan sát được, chẳng hạn như các đối tượng LiveData. Nếu ViewModel cần ngữ cảnh Application, chẳng hạn như để tìm một dịch vụ hệ thống, thì đối tượng có thể mở rộng lớp AndroidViewModel và có một hàm khởi tạo nhận được Application trong hàm khởi tạo, vì lớp Application mở rộng Context.

Vòng đời của một ViewModel

Các đối tượng ViewModel nằm trong phạm vi của Lifecycle được truyền đến ViewModelProvider khi lấy ViewModel. ViewModel sẽ vẫn lưu trong bộ nhớ cho đến khi phạm vi Lifecycle mà đối tượng nằm trong biến mất vĩnh viễn: trong trường hợp của một hoạt động là khi hoạt động kết thúc, còn trong trường hợp của một mảnh là khi mảnh được tách ra.

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 toàn bộ thời gian hoạt động 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 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ỷ.

Chia sẻ dữ liệu giữa các mảnh

Thông thường, hai hoặc nhiều mảnh trong một hoạt động cần giao tiếp với nhau. Hãy tưởng tượng một trường hợp phổ biến là mảnh chế độ xem phân tách (list-detail), khi bạn có một mảnh mà trong đó người dùng chọn một mục trong danh sách và một mảnh khác hiển thị nội dung của mục đã chọn. Trường hợp này luôn quan trọng vì cả hai mảnh đều cần xác định một số nội dung mô tả giao diện và hoạt động chủ phải liên kết cả hai với nhau. Ngoài ra, cả hai mảnh đều phải xử lý trường hợp mảnh khác chưa được tạo hoặc xuất hiện.

Bạn có thể giải quyết vấn đề thường gặp này bằng cách sử dụng các đối tượng ViewModel. Những mảnh này có thể chia sẻ một ViewModel bằng cách sử dụng phạm vi hoạt động của chúng để xử lý quá trình giao tiếp, được minh hoạ trong mã mẫu sau:

Kotlin

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class ListFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

Java

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class ListFragment extends Fragment {
    private SharedViewModel model;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), item -> {
           // Update the UI.
        });
    }
}

Xin lưu ý rằng cả hai mảnh sẽ truy xuất hoạt động có chứa chúng. Bằng cách đó, khi mỗi mảnh nhận được ViewModelProvider, chúng sẽ nhận được cùng một thực thể SharedViewModel thuộc phạm vi của hoạt động này.

Phương pháp này mang lại những lợi ích sau:

  • Hoạt động không cần phải làm gì hoặc không cần biết gì về quá trình giao tiếp này.
  • Các mảnh không cần phải biết về nhau ngoài hợp đồng SharedViewModel. Nếu một trong các mảnh biến mất, thì mảnh còn lại sẽ tiếp tục hoạt động như bình thường.
  • Mỗi mảnh có vòng đời riêng và không bị vòng đời của mảnh kia ảnh hưởng. Nếu một mảnh thay thế mảnh còn lại, thì giao diện người dùng sẽ tiếp tục hoạt động mà không gặp bất kỳ sự cố nào.

Thay thế Trình tải bằng ViewModel

Các lớp trình tải như CursorLoader thường được dùng để duy trì việc đồng bộ hoá dữ liệu trong giao diện người dùng của một ứng dụng với cơ sở dữ liệu. Bạn có thể dùng ViewModel cùng với một số lớp khác để thay thế trình tải. Việc sử dụng ViewModel sẽ tách bộ điều khiển giao diện người dùng khỏi hoạt động tải dữ liệu, đồng nghĩa với việc bạn sẽ có ít tệp tham chiếu mạnh giữa các lớp hơn.

Một phương pháp phổ biến để sử dụng trình tải là ứng dụng có thể sử dụng một CursorLoader để quan sát nội dung của cơ sở dữ liệu. Khi một giá trị trong cơ sở dữ liệu thay đổi, thì trình tải sẽ tự động kích hoạt quá trình tải lại dữ liệu và cập nhật giao diện người dùng:

Hình 2. Tải dữ liệu bằng trình tải

ViewModel hoạt động với RoomLiveData để thay thế trình tải. ViewModel đảm bảo rằng dữ liệu được duy trì sau khi thay đổi cấu hình thiết bị. Room thông báo về LiveData của bạn khi cơ sở dữ liệu thay đổi và LiveData sau đó sẽ cập nhật giao diện người dùng với dữ liệu đã sửa đổi.

Hình 3. Tải dữ liệu bằng ViewModel

Sử dụng coroutine với ViewModel

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

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ó sự 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ó nhiều 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.

Mẫu

Lớp học lập trình

Blog

Video