+1

Mastering Android ViewModels : Những điều nên và không nên - Part 2

Ở bài viết trước chúng ta đã cùng nhau nói về việc tại sao không nên khởi tạo trạng thái trong khối Init{} block

Hãy cùng xem các điểm thảo luận chính của loạt bài này và ôn lại nhé. Trong phần này, chúng ta sẽ thảo luận về #2 và #3 trong danh sách bên dưới. Có thể bình thường bạn đã code như vậy rồi và nghĩ rằng ai chả làm vậy. Nhưng hãy cùng phân tích rõ hơn tại sao phải làm như thế nhé

  1. Tránh khởi tạo trạng thái trong khối init {}.(link bài viết đây nha)
  2. Tránh để lộ các mutable states.
  3. Sử dụng update{} khi sử dụng MutableStateFlows:
  4. Lazily inject dependencies trong constructor.
  5. Sử dụng code reactive hơn và ít ràng buộc hơn.
  6. Tránh khởi tạo ViewModel từ bên ngoài.
  7. Tránh truyền tham số từ bên ngoài.
  8. Tránh hardcode Coroutine Dispatchers.
  9. Unit test ViewModels.
  10. Tránh để lộ suspended functions.
  11. Tận dụng onCleared() callback trong ViewModels.
  12. Xử lý sự cố của quá trình và thay đổi cấu hình.
  13. Inject các UseCase trong Repositories, sau đó gọi các DataSource .
  14. Chỉ include domain objects trong ViewModels.
  15. Tận dụng các toán tử shareIn() và stateIn() để tránh phải upstream nhiều lần.

#2 Tránh để lộ các mutable states ❌:

Việc hiển thị MutableStateFlow trực tiếp từ Android ViewModels có thể gây ra một số vấn đề liên quan đến architecture, tính toàn vẹn dữ liệu và khả năng duy trì tổng thể của code. Dưới đây là một số mối bận tâm chính:

Vi phạm tính đóng gói:

Vấn đề chính khi phơi bày một MutableStateFlow là nó phá vỡ nguyên tắc đóng gói của lập trình hướng đối tượng. Bằng cách hiển thị một thành phần có thể thay đổi, bạn cho phép các lớp bên ngoài sửa đổi trạng thái trực tiếp, điều này có thể dẫn đến hành vi ứng dụng không thể đoán trước, các lỗi khó theo dõi và vi phạm trách nhiệm của ViewModel trong việc quản lý và kiểm soát trạng thái của chính nó.

Rủi ro về tính toàn vẹn dữ liệu:

Khi các lớp bên ngoài có thể sửa đổi trạng thái một cách trực tiếp, việc duy trì tính toàn vẹn của dữ liệu trở nên khó khăn. ViewModel không còn có thể đảm bảo rằng các chuyển đổi trạng thái của nó là hợp lệ, có khả năng dẫn đến các trạng thái không hợp lệ hoặc không nhất quán trong ứng dụng. Điều này có thể làm phức tạp việc quản lý nhà nước và tăng nguy cơ xảy ra lỗi.

Tăng độ phức tạp: Việc cho phép sửa đổi trạng thái trực tiếp từ bên ngoài ViewModel có thể dẫn đến code base phức tạp hơn. Việc theo dõi các thay đổi trạng thái được bắt đầu ở đâu và như thế nào trở nên khó khăn hơn, khiến code base khó hiểu và khó bảo trì hơn. Điều này cũng có thể khiến việc debug trở nên khó khăn hơn vì không rõ bằng cách nào ứng dụng đạt đến trạng thái cụ thể.

Các vấn đề Concurrency:

MutableStateFlow an toàn theo luồng nhưng việc quản lý concurrency trở nên phức tạp hơn khi nhiều phần của ứng dụng có thể cập nhật trạng thái 1 cách đồng thời. Nếu không có sự phối hợp cẩn thận, bạn có thể gặp phải tình trạng tương tranh hoặc các vấn đề concurrency khác dẫn đến hành vi ứng dụng không ổn định.

Khó khăn về test

Việc kiểm tra ViewModel trở nên khó khăn hơn khi trạng thái bên trong của nó có thể được sửa đổi từ bên ngoài. Việc dự đoán và kiểm soát trạng thái của ViewModel trong các thử nghiệm khó hơn, điều này có thể khiến các thử nghiệm kém tin cậy hơn và phức tạp hơn.

Kiến trúc rõ ràng:

Việc hiển thị trực tiếp các mutable states có thể làm mờ ranh giới giữa các lớp khác nhau trong kiến trúc ứng dụng của bạn. Vai trò của ViewModel là hiển thị dữ liệu và xử lý logic để UI quan sát và phản ứng, không cung cấp nguồn dữ liệu có thể thay đổi và có thể thay đổi từ bất kỳ đâu. Điều này có thể dẫn đến sự phân tách các mối quan tâm ít rõ ràng hơn, làm cho kiến trúc khó hiểu và khó theo dõi hơn.

Thiếu kiểm soát đối với người quan sát:

Khi trạng thái có thể được sửa đổi từ bên ngoài, việc kiểm soát cách thức và thời điểm người quan sát được thông báo về các thay đổi sẽ khó hơn. Điều này có thể dẫn đến cập nhật UI không cần thiết hoặc bị bỏ lỡ nếu trạng thái bị thay đổi mà không thông báo chính xác cho người quan sát.

Dưới đây là một ví dụ triển khai chưa hợp lý:

class RatesViewModel constructor(
    private val ratesRepository: RatesRepository,
) : ViewModel() {
    val state = MutableStateFlow(RatesUiState(isLoading = true))
}

OK, vậy làm sao không để lộ mutable state 🤔

Để giảm thiểu những vấn đề này, bạn nên hiển thị trạng thái từ ViewModels ở dạng chỉ đọc bằng StateFlow hoặc LiveData. Cách tiếp cận này duy trì tính đóng gói và cho phép ViewModel quản lý trạng thái của nó hiệu quả hơn. Các thay đổi đối với trạng thái có thể được thực hiện thông qua các phương thức được xác định rõ ràng trong ViewModel, có thể xác thực và xử lý các thay đổi nếu cần. Điều này giúp đảm bảo tính toàn vẹn dữ liệu, đơn giản hóa việc kiểm tra và duy trì kiến trúc rõ ràng.

class RatesViewModel constructor(
    private val ratesRepository: RatesRepository,
) : ViewModel() {

    private val _state = MutableStateFlow(RatesUiState(isLoading = true))

    val state: StateFlow<RatesUiState>
        get() = _state.asStateFlow()

}

Trong ví dụ trên, chúng ta có internal private state cho ViewModel, state này có thể được cập nhật nội bộ và sau đó chúng ta hiển thị trạng thái bất biến bằng cách sử dụng extension function asStateFlow().

#3 Sử dụng update{} khi sử dụng MutableStateFlows 📜:

Việc sử dụng MutableStateFlow trong Kotlin, đặc biệt là trong context Android, mang đến một cách xử lý linh hoạt để xử lý dữ liệu có thể thay đổi theo thời gian. Khi bạn cần cập nhật trạng thái được đại diện bởi MutableStateFlow, thực tế bạn có thể thực hiện một số phương pháp khác để cập nhật. Hãy cùng khám phá những phương pháp này và lý do tại sao nên sử dụng .update{} nhé.

Cách 1: Asignment trực tiếp

mutableStateFlow.value = mutableStateFlow.value.copy()

Cách này liên quan đến việc trực tiếp thiết lập giá trị của MutableStateFlow bằng cách tạo bản sao của trạng thái hiện tại với những thay đổi mong muốn. Cách tiếp cận này đơn giản và hoạt động tốt đối với các cập nhật trạng thái đơn giản. Tuy nhiên, đó không phải là atomic 🛑🛑🛑, có nghĩa là nếu nhiều luồng đang cập nhật trạng thái đồng thời, bạn có thể gặp phải tình trạng tương tranh.

Dành cho bạn nào chưa hiểu khái niệm aomic thì nó đề cập đến tính chất của một hoặc một nhóm các hoạt động mà không thể bị chia cắt hoặc bị can thiệp bởi các luồng khác trong môi trường đa luồng (multithreading). Trong ngữ cảnh này, "atomic" ám chỉ rằng hoạt động đó hoặc được thực hiện hoàn toàn, hoặc không được thực hiện tại một thời điểm nhất định.

Các hoạt động "atomic" là quan trọng trong lập trình đa luồng vì chúng đảm bảo tính nhất quán và an toàn của dữ liệu khi nhiều luồng cùng truy cập và sửa đổi cùng một biến hoặc tài nguyên.

Ví dụ, trong Java, bạn có thể sử dụng từ khóa synchronized hoặc các lớp từ gói java.util.concurrent.atomic để đảm bảo tính "atomic" của các hoạt động đối với các biến nguyên (int, long, boolean, vv.).

Tùy chọn 2: Emit trạng thái mới

mutableStateFlow.emit(newState())

Việc sử dụng .emit() cho phép bạn gửi trạng thái mới vào MutableStateFlow. Mặc dù .emit() an toàn theo luồng và có thể được sử dụng để cập nhật đồng thời, nhưng đó là suspending function . Điều này có nghĩa là nó phải được gọi trong một coroutine và được thiết kế cho các tình huống mà bạn có thể cần đợi trạng thái được sử dụng. Điều này có thể linh hoạt hơn nhưng cũng gây ra sự phức tạp khi được sử dụng trong các khối synchronous code blocks hoặc bên ngoài coroutine.

Tùy chọn 3: Sử dụng .update{}

mutableStateFlow.update { it.copy(// sửa đổi trạng thái tại đây) }

Tại sao .update{} thường là phương pháp được ưu tiên:

  • Tính atomic: .update{} đảm bảo rằng thao tác cập nhật là atomic, điều này rất quan trọng trong môi trường concurrent. Tính chất này đảm bảo rằng mỗi bản cập nhật được áp dụng dựa trên trạng thái mới nhất, tránh xung đột giữa các bản cập nhật đồng thời.
  • An toàn luồng: Nó quản lý an toàn thread trong nội bộ, do đó bạn không phải lo lắng về việc synchronizing state trên các luồng khác nhau.
  • Đơn giản và an toàn: Nó cung cấp một cách đơn giản và an toàn để cập nhật trạng thái mà không cần phải quản lý coroutine một cách rõ ràng, như trường hợp của .emit() đối với các bản cập nhật không đồng bộ (non-synchronous).

Tóm lại, mặc dù phép gán trực tiếp và .emit() có các trường hợp sử dụng riêng, nhưng .update{} được thiết kế để cung cấp một cách atomic, an toàn theo luồng để cập nhật các giá trị MutableStateFlow. Điều này làm cho nó trở thành một lựa chọn tuyệt vời cho hầu hết các tình huống mà bạn cần đảm bảo cập nhật nhất quán và an toàn cho reactive state của mình trong môi trường concurrent.

Cách sử dụng

Hãy tưởng tượng bạn có MutableStateFlow đang giữ trạng thái của Use là 1 data class

data class User(val name: String, val age: Int)
val userStateFlow = MutableStateFlow(User(name = "John", age = 30))

Nếu bạn muốn cập nhật tuổi của người dùng, bạn có thể làm:

userStateFlow.update { currentUser ->
    currentUser.copy(age = currentUser.age + 1)
}

Tóm lại 📝:

Chúng ta đã chỉ ra các kỹ thuật tiên tiến quan trọng để phát triển ứng dụng hiệu quả. Chúng ta đã nêu bật những cạm bẫy của việc hiển thị trực tiếp trạng thái có thể thay đổi từ ViewModels và thảo luận về các rủi ro liên quan. Để giải quyết những thách thức này, chúng ta đã đề xuất các giải pháp như sử dụng read-only state và tận dụng function update{} để cập nhật trạng thái an toàn hơn, đảm bảo base code vẫn mạnh mẽ và có thể duy trì được.

Bằng cách tránh những lỗi phổ biến và sử dụng các kỹ thuật phù hợp, chúng ta có thể làm cho ứng dụng của mình mạnh mẽ hơn, giữ an toàn cho dữ liệu và giúp việc kiểm tra dễ dàng hơn. Vì vậy, hãy nhớ làm theo những mẹo này để xây dựng ứng dụng Android tốt hơn nhé !!!! 🥰

Bài viết xin kết tại đây. Cảm ơn mn đã quan tâm !!!

Nguồn : https://proandroiddev.com/mastering-android-viewmodels-essential-dos-and-donts-part-2-️-2b49281f0029


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.