Kiến thức Android ViewModel - Android Mastery by Dan Tech
Kiến thức Android ViewModel - Android Mastery by Dan Tech

Android ViewModel: Kiến thức cần biết và các công cụ đi kèm

Vậy là chúng ta đã tìm hiểu và tường minh được cả 3 mô hình kiến trúc MVC, MVP, MVVM. Kể từ chương này về sau chúng ta sẽ chỉ nói về MVVM vì các ưu thế của nó và tính ứng dụng cao trong thị trường việc làm hiện tại.

Trong MVVM Android, mấu chốt lớn nhất là ViewModel. Nơi đây tổ chức các logic, điều hướng các sự kiện và trả về kiểu dữ liệu phù hợp. Việc thành thạo cách tổ chức code trong ViewModel và ứng dụng nó với các thư viện LiveData, SharedFlow, StateFlow, hay RxJava sẽ giúp cho code của bạn dễ đọc hơn, dễ bảo trì và viết Unit Test hơn.

Bạn có thể skip phần đọc Blog này nếu đã có kinh nghiệm và nhảy vào đọc code luôn tại GitHub của mình.

Lifecycle của Android ViewModel

Mỗi ViewModel được tạo ra sẽ cần có một ViewModelStoreOwner. ViewModelStoreOwner là nơi để lưu trữ các ViewModel theo cấu trúc Map (key - value) và trực tiếp quản lý vòng đời của các ViewModel đó.

Bạn có thể tự tạo ViewModel thông qua constructor và tự quản lý theo mục đích cá nhân. Tuy nhiên đó sẽ là một cách làm ngu ngốc, tốn thời gian của bạn và cả những người đọc code sau này.

100% tôi khuyên bạn hãy dùng ViewModelStoreOwner từ Activity, hoặc Fragment mà bạn cần là được.

Activity kế thừa ViewModelStoreOwner
Fragment kế thừa ViewModelStoreOwner

Một điều đặc biệt trong hệ điều hành Android, là khi Activity hoặc Fragment bị destroyre-create do sự kiện configuration change (không phải là chủ đích của người dùng). Thì data của ViewModelStoreOwner vẫn được giữ nguyên. Chính vì vậy dữ liệu trên Android ViewModel trong trường hợp này vẫn còn và sẽ được sử dụng lại ngay khi Activity / Fragment re-create thành công.

Lý giải cho điều này rất dài dòng, tuy nhiên bạn có thể hiểu trong 3 câu sau đây

  • ViewModel được lưu trữ trong ViewModelStoreOwner
  • Khi có sự kiện Destroy, nếu đang có Configuration Changes thì biến viewModelStore trong Activity sẽ không clear data
viewModelStore không clear khi có sự kiện Configuration Change
  • ViewModelStoreOwner được lưu trữ trong NonConfigurationInstances , và luôn được check trong mỗi lần get
ViewModelStore được restore từ NonConfigurationInstance mỗi lần ensureViewModelStore

Hãy nắm vững điều này, và ghi nhớ chúng. Rất nhiều lập trình viên Android biết rằng ViewModel được giữ lại sau các sự kiện Configuration Change nhưng họ không thể hiểu và giải thích được tại sao Activity bị destroy rồi tạo lại vẫn có thể phục hồi nguyên dữ liệu của ViewModelStoreOwner.

Tất cả các biến bên trong một Android ViewModel đều có vòng đời của ViewModel chứa nó. Tận dụng lợi thế này lập trình viên có thể sử dụng LiveData, SharedFlow, StateFlow, hoặc RxJava để phục vụ mục đích phát triển tính năng của mình.

Phần tiếp theo Danh sẽ hướng dẫn bạn học và hiểu các công cụ này. Và biết cách dùng chúng trong từng trường hợp cụ thể.

RxJava

RxJava là một thư viện lâu đời, được viết bằng ngôn ngữ Java. Dùng được cho cả BackendAndroid app.

RxJava giúp đơn giản hóa việc thao tác trên Background Thread và return giá trị về Main Thread.

// Example if we don't use RxJava
bgThreadHandler.post{
	try{
		val result = heavyCalculation()
		mainThreadHandler.post{
			textView.text = "$result"
		}
	} catch(e: Exception) {
		Log.d("Thread", "bgThread error $e")
	}
}

// Example if we use RxJava
Single.create(emitter -> {
	try{
		emitter.onSuccess(heavyCalculation())
	}catch(e: Exception){
			emitter.onError(e)
	}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
	textView.text = "$result"
}, { e ->
	Log.d("RxJava", "Single Error $e")
});

Ngoài sự tiện lợi và an toàn trên, RxJava còn cung cấp cho chúng ta nhiều toán tử (operator) để thao tác trên các giá trị trả về của mỗi RxJava Stream để tạo ra kết quả như mong muốn.

val numbers = Observable.just(1, 2, 3, 4, 5)
numbers
	.map { it * 2 } // map operator
  .subscribe { println(it) }

val letters = Observable.just("A", "B", "C")
letters
    .flatMap { letter -> 
        Observable.just("${letter}1", "${letter}2") // flatMap operator
    }
    .subscribe { println(it) }

Một số loại stream mà RxJava hỗ trợ:

  • Observable
  • Single
  • PublishSubject, BehaviorSubject, Flowable

LiveData

LiveData cũng là một dạng Observable (là Object mà ta có thể quan sát được sự thay đổi giá trị của nó), khi observe lên LiveData , ta có thể lắng nghe được sự thay đổi các giá trị của LiveData qua mỗi lần cập nhật.

Điều đặc biệt cũng là giới hạn của LiveData đó là chúng chỉ được sử dụng trong phạm vi của Activity, Fragment lifecycle.

Một điều thú vị khác, khi thực hiện observe lên LiveData thì hàm callback sẽ luôn nhận được giá trị gần nhất của LiveData. Điều này giúp cho việc cập nhật các giao diện theo Data trở nên dễ dàng trong lifecycle của Fragment, Activity.

liveData.observe(lifecycle) { it ->
	Log.d("LiveDataObserver", "$it")
} // Thực hiện obseve lên LiveData

liveData.value = 0 // set value cho LiveData trên Main Thread
liveData.postValue(1) // set value cho LiveData trên background Thread

LiveData, tất cả các sự kiện cập nhật giá trị đều được chạy ở Main Thread. Tuy nhiên việc set giá trị cho LiveData có thể được thực hiện ở cả Main Thread lẫn Background Thread.

  • liveData.value = 0 —> đây là cách gán giá trị cho một LiveData khi đang ở Main Thread. Nếu gọi logic này ở Background Thread, sẽ crash chương trình.
  • liveData.postValue(1) —> đây là cách có thể dùng ở bất kỳ Thread nào. Giá trị được gán trong hàm postValue sẽ được đưa lên Main Thread và gọi lại hàm liveData.value sau đó.

Kotlin Coroutines

Coroutines là một thư viện mới đến từ ngôn ngữ Kotlin, chạy được ổn định trên JVM cũng như Android và các môi trường hỗ trợ ngôn ngữ Kotlin.

Cách tiếp cận của Kotlin Coroutines gần tương tự như RxJava – cả 2 đều sử dụng Thread Pool, và switch thread khi cần thiết. Tuy nhiên Kotlin Coroutines ở 1 mức độ phù hợp cao hơn, tiết kiệm tài nguyên hơn và có các tính năng thú vị hơn.

Để sử dụng tốt Koltin Coroutines ta cần phải nắm qua các định nghĩa sau

  • CoroutineScope: Đây là lớp đại diện cho môi trường thực thi của các Kotlin Coroutines. Bạn có thể hiểu đơn giản CoroutineScope là lớp dùng để quản lý vòng đời, thứ tự thực thi và trạng thái của các Job bên trong nó.
  • Job: Đây có thể hiểu là đối tượng thực thi trên CoroutineScope (tương tự như Runnable trong Java Thread)
  • Dispatcher: Đây là lớp dùng để xác định xem Job của bạn khi thực thi trong CoroutineScope sẽ chạy trên Thread nào. Thông thường chúng ta sẽ dùng 1 trong 2 Dispatchers.MainDispatchers.IO
  • suspend function: là 1 loại function mà khi thực thi có khả năng tạm dừng, đổi thread, tiếp tục thực thi mà không block thread hiện tại. Bạn hãy đọc tiếp phần withContext để hiểu hơn về cách sử dụng
  • withContext: Đây là một suspend function dùng để switch Context (có thể hiểu là chuyển Thread). Điều thú vị ở đây là khi ta sử dụng withContext thì suspend function bên ngoài sẽ tạm dừng để đợi kết quả từ withContext rồi mới thực thi tiếp.
// Cach viet su dung Java Thread
mainHandler.post{
	textView.text = "Begin"
	backgroundThreadHandler.post {
		val heavyCalculation = heavyCalculation()
		mainHandler.post {
			textView.text = "Result $result"
		}
	}
}

// Cach viet su dung Kotlin Coroutines x suspend function
coroutineScope.launch(Dispatchers.Main) {
	textView.text = "Begin"
	val result = withContext(Distpatchers.IO){
		heavyCalculation()
	}
	textView.text = "Result $result"
}
  • launch : Là một extension function của CoroutineScope. Launch được xem như là điểm entry để ta gọi được các suspend function từ môi trường bình thường. Khi sử dụng launch ta sẽ không kỳ vọng có thể nhận được giá trị trả về
  • async await : async là một extension function của CoroutineScope. async khác biệt với launch, async có thể nhận giá trị trả về thông qua suspend function await
lifecycleScope.launch(Dispatchers.IO) {
    val task1 = async {
        heavyTask1()
    }
    val task2 = async {
        heavyTask2()
    }
    Log.d("CampaignFragment", "task1: ${task1.await()} task2: ${task2.await()}")
}

Flow, SharedFlow, StateFlow

Flow là base, đại diện cho các Observable Flow. Các Flow có khả năng emit data cho các subscriber collect và xử lý.

val sampleFlow = flow {
    var i = 0
    while (true) {
        delay(1000)
        emit(i++)
    }
}

lifecycleScope.launch {
    sampleFlow.collect {
        Log.d("SampleFlow", "collected $it")
    }
}

SharedFlow

SharedFlow là Flow nhưng có 1 số tính chất đặc biệt

  • SharedFlow khởi tạo không cần giá trị bắt đầu
val stateFlow: MutableStateFlow<Int> = MutableStateFlow<Int>(0)
  • replay: ta có thể set giá trị này n ≥ 0 để khi SharedFlow có subscriber mới thì subscriber đó sẽ ngay lập tức nhận được n giá trị gần nhất của SharedFlow theo đúng thứ tự trước đó.
  • Khi SharedFlow emit 2 giá trị bằng nhau liên tiếp, các subscriber sẽ nhận được lần lượt 2 giá trị. (not distinct on change)

StateFlow

  • StateFlow khởi tạo cần có giá trị bắt đầu
val stateFlow: MutableStateFlow<Int> = MutableStateFlow<Int>(0)
  • Mặc định replay = 1
  • Mặc định distinct Until Change (không cho phép emit 2 giá trị bằng nhau liên tục)

SharedFlow, StateFlow được ngầm công nhận là sinh ra để thay thế LiveData trong Android. Rất nhiều người biết điều đó nhưng không thực sự trả lời được cho câu hỏi Tại sao SharedFlow, StateFlow lại có thể thay thế LiveData?