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.
Một điều đặc biệt trong hệ điều hành Android, là khi Activity hoặc Fragment bị destroy
và re-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
- ViewModelStoreOwner được lưu trữ trong NonConfigurationInstances , và luôn được check trong mỗi lần get
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ả Backend
và Android
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ộtLiveData
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àmpostValue
sẽ được đưa lênMain Thread
và gọi lại hàmliveData.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ácKotlin Coroutines
. Bạn có thể hiểu đơn giảnCoroutineScope
là lớp dùng để quản lý vòng đời, thứ tự thực thi và trạng thái của cácJob
bên trong nó.Job
: Đây có thể hiểu là đối tượng thực thi trênCoroutineScope
(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.Main
vàDispatchers.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ầnwithContext
để hiểu hơn về cách sử dụngwithContext
: Đây là mộtsuspend function
dùng để switch Context (có thể hiểu là chuyển Thread). Điều thú vị ở đây là khi ta sử dụngwithContext
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ủaCoroutineScope
. Launch được xem như là điểm entry để ta gọi được cácsuspend 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ủaCoroutineScope
. async khác biệt vớilaunch
,async
có thể nhận giá trị trả về thông quasuspend 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
?