Mastering Android ViewModels: Essential Dos and Donāts Part 5 š ļø5ļøā£
Mastering Android ViewModels: Essential Dos and Donāts Part 5 š ļø5ļøā£ ź“ė Ø
This will be the fifth installment in our seriesĀ āMastering Android ViewModels
āĀ where we dive deep into the essential dos and donāts that can elevate your Android development skills. Weāve already covered several tips to improve performance and code quality in ViewModels
, which have become an integral part of modern Android applications.
Mastering Android ViewModels: Essential Dos and Donāts Series ššš
- Avoid initializing the state in the
init {}
block.Ā ā Read here - Avoid exposing mutable states.Ā ā Ā Read here
- Use
update{}
when usingMutableStateFlows
.Ā ā Ā Read here - Try not to import Android dependencies in the
ViewModels
.Ā ā Ā Read here - Lazily inject dependencies in the constructor.Ā ā Ā Read here
- Embrace more reactive and less imperative coding. ā Read here
- Avoid initializing the
ViewModel
from the outside world. ā Read here
In this article weāll cover
1.Ā Avoid hardcoding Coroutine Dispatchers. 2.Ā Unit test your ViewModels. 3.Ā Avoid exposing suspended functions.
1. Avoid Hardcoding Coroutine Dispatchers
When dealing with coroutines in your ViewModel
, hardcoding dispatchers likeĀ Dispatchers.IO
Ā orĀ Dispatchers.Default
Ā might seem convenient, but it can lead to tightly coupled and less testable code.
The Problem with Hardcoding Dispatchers
Hardcoding dispatchers directly in your ViewModel
can make testing difficult and reduce flexibility. For instance, during testing, you may want to control the threading behavior, which becomes challenging with hardcoded dispatchers.
Recommended Approach
Inject your dispatchers via the constructor or use a dependency injection framework like Hilt or Dagger. This not only makes your ViewModel
more flexible but also simplifies testing:
class MyViewModel(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
private fun loadData() {
viewModelScope.launch(ioDispatcher) {
// Your coroutine code here
}
}
}
By using dependency injection, you can swap out the dispatcher during testing, ensuring your ViewModel
behaves correctly in different environments.
for an example look at:
package com.example.words_list
import androidx.paging.testing.asSnapshot
import assertk.Assert
import assertk.assertThat
import assertk.assertions.isTrue
import assertk.assertions.size
import com.example.data.sync.DictionarySyncStateWatcherDefault
import com.example.domain.repository.DictionaryRepository
import com.example.domain.usecases.GetFilteredWordsUseCase
import com.example.testing.DataSyncStatusFake
import com.example.testing.FakeDictionaryRepo
import com.example.testing.MainDispatcherRule
import com.example.wordslist.WordsListViewModel
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class WordsListViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun testViewModelLoadsData() = runTest(timeout = 2.seconds) {
val fakeRepository: DictionaryRepository = FakeDictionaryRepo(createWordsSequence(size = 1_000))
val viewModel = WordsListViewModel(
getFilteredWordUseCase = GetFilteredWordsUseCase(fakeRepository),
stateWatcher = com.example.data.sync.DictionarySyncStateWatcherDefault(DataSyncStatusFake()),
)
val items = viewModel.pagingDataFlow
val itemsSnapshot = items.asSnapshot {
// Scroll to the 50th item in the list. This will also suspend till
// the prefetch requirement is met if there's one.
// It also suspends until all loading is complete.
scrollTo(index = 300)
}
assertThat(itemsSnapshot.size >= 300).isTrue()
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 500) }).size >= 500).isTrue()
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 700) }).size >= 700).isTrue()
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 850) }).size >= 850).isTrue()
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 900) }).size >= 900).isTrue()
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 1000) })).transform { it.size >= 1000 }.isTrue()
}
@Test
fun testViewModelLoadsHugeData() = runTest {
val fakeRepository: DictionaryRepository = FakeDictionaryRepo(createWordsSequence(size = 1000_000))
val viewModel = WordsListViewModel(
getFilteredWordUseCase = GetFilteredWordsUseCase(fakeRepository),
stateWatcher = com.example.data.sync.DictionarySyncStateWatcherDefault(DataSyncStatusFake()),
)
val items = viewModel.pagingDataFlow
val itemsSnapshot = items.asSnapshot {
scrollTo(index = 300)
}
assertThat(itemsSnapshot).isAtLeast(422)
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 500) })).isAtLeast(500)
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 700) })).isAtLeast(700)
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 850) })).isAtLeast(850)
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 10_000) })).isAtLeast(10_000)
assertThat(items.asSnapshot(loadOperations = { scrollTo(index = 12_000) })).isAtLeast(12_000)
}
private fun createWordsSequence(size: Int) = (1..size).map { "$it" }.asSequence()
fun Assert<List<*>>.isAtLeast(size: Int) {
size().transform { it >= size }.isTrue()
}
}
2. Unit Test Your ViewModels
Unit testing is essential to ensure your ViewModels
behave as expected. Without proper tests, you risk introducing bugs that could have been caught early.
Testing Challenges
ViewModels
often interact with complex state and other components, making them tricky to test. However, by following the right practices, specially what we discuss in this series, you can thoroughly test your ViewModelās logic.
Best Practices for Testing ViewModels
- Use a
TestCoroutineDispatcher
Ā to control coroutine execution and test asynchronous code synchronously. - Favor testing ViewModels as a non-Android test (use test folder instead of androidTest)
- Avoid usingĀ
runBlocking{}
Ā for testingĀsuspended
Ā functions, instead useĀrunTest{}
Ā fromĀcoroutines-test
- Avoid manually peeking values fromĀ
StateFlows
, UseĀcashapp/turbine
Ā instead - For testingĀ
flows
, useĀcashapp/turbine
- Favor fakes over mocks
3. Avoid Exposing Suspended Functions
WhileĀ suspend
Ā functions make asynchronous programming in Kotlin easier, exposing them directly from your ViewModel can lead to misuse and increased complexity.
Why Itās Problematic
ExposingĀ suspend
Ā functions can result in mismanagement of threading or lifecycle events, leading to bugs or crashes.
The Better Way
Keep suspension internal to the ViewModel, and expose results throughĀ Flow
Ā or other observable patterns.
Conclusion
Mastering ViewModels
in Android development is crucial for creating robust, efficient, and maintainable applications. Throughout this series, weāve discussed a comprehensive set of best practices to improve your code quality and application performance.
šĀ CongratulationsĀ if youāve made it this far in the article! šĀ Donāt forget to:
- š Smash the clap button as many times! So I can continue with the follow-up articles!
- FollowĀ my YouTube channel (
DroidFly
)Ā for video tutorials and tips on Android development - āØāØĀ If you need help with your Android ViewModels, Project, or your career development, Book a 1:1 or a Pair-Programming session with me, Book a time now š§āš»š§āš»š§āš»
- check out the previous articles in this series with the links below:
Info
This article is previously published on proandroiddev.com