Lightweight framework for managing ViewModels in the same way as Lich Component.
This library is very similar to AndroidX ViewModel, but it has the following advantages:
- The code for initializing a ViewModel class can be written in the ViewModel itself.
- It is easy to write unit tests for Activity / Fragment with mocking ViewModels.
Add the following entries to your build.gradle
file.
dependencies {
implementation 'com.linecorp.lich:viewmodel:x.x.x'
// For Jetpack Compose
implementation 'com.linecorp.lich:viewmodel-compose:x.x.x'
testImplementation 'com.linecorp.lich:viewmodel-test-mockk:x.x.x'
testImplementation 'androidx.test:runner:x.x.x'
testImplementation 'androidx.test.ext:junit:x.x.x'
testImplementation 'io.mockk:mockk:x.x.x'
testImplementation 'org.robolectric:robolectric:x.x'
androidTestImplementation 'com.linecorp.lich:viewmodel-test-mockk:x.x.x'
androidTestImplementation 'androidx.test:runner:x.x.x'
androidTestImplementation 'androidx.test.ext:junit:x.x.x'
androidTestImplementation 'io.mockk:mockk-android:x.x.x'
}
The above code uses MockK as a mocking library. If you prefer Mockito-Kotlin over MockK, you can specify the following instead.
dependencies {
implementation 'com.linecorp.lich:viewmodel:x.x.x'
// For Jetpack Compose
implementation 'com.linecorp.lich:viewmodel-compose:x.x.x'
testImplementation 'com.linecorp.lich:viewmodel-test-mockitokotlin:x.x.x'
testImplementation 'androidx.test:runner:x.x.x'
testImplementation 'androidx.test.ext:junit:x.x.x'
testImplementation 'org.mockito:mockito-inline:x.x.x'
testImplementation 'org.mockito.kotlin:mockito-kotlin:x.x.x'
testImplementation 'org.robolectric:robolectric:x.x'
androidTestImplementation 'com.linecorp.lich:viewmodel-test-mockitokotlin:x.x.x'
androidTestImplementation 'androidx.test:runner:x.x.x'
androidTestImplementation 'androidx.test.ext:junit:x.x.x'
androidTestImplementation 'org.mockito:mockito-android:x.x.x'
androidTestImplementation 'org.mockito.kotlin:mockito-kotlin:x.x.x'
}
In this library, your ViewModel classes need to extend AbstractViewModel and have a companion object inheriting ViewModelFactory.
This is a sample code:
class FooViewModel(context: Context, savedStateHandle: SavedStateHandle) : AbstractViewModel() {
// snip...
companion object : ViewModelFactory<FooViewModel>() {
override fun createViewModel(context: Context, savedStateHandle: SavedStateHandle): FooViewModel =
FooViewModel(context, savedStateHandle)
}
}
You can obtain an instance of the ViewModel using ComponentActivity.viewModel or Fragment.viewModel like this:
class FooActivity : AppCompatActivity() {
// An instance of FooViewModel associated with FooActivity.
private val fooViewModel by viewModel(FooViewModel)
// snip...
}
class FooFragment : Fragment() {
// An instance of FooViewModel associated with FooFragment.
private val fooViewModel by viewModel(FooViewModel)
// snip...
}
You can also use Fragment.activityViewModel to obtain a ViewModel associated with the Activity hosting the current Fragment. This ViewModel can be used to share data between Fragments and their host Activity.
class FooFragment : Fragment() {
// A shared instance of FooViewModel associated with the Activity hosting this FooFragment.
private val fooActivityViewModel by activityViewModel(FooViewModel)
// snip...
}
If you're using AndroidX Navigation library, you can use Fragment.navGraphViewModel to obtain a ViewModel scoped to the entry point's navigation back stack.
class FooFragment : Fragment() {
// A shared instance of FooViewModel scoped to the `foo_nav_graph` navigation graph.
private val fooNavGraphViewModel by navGraphViewModel(FooViewModel, R.id.foo_nav_graph)
// snip...
}
You can also use Lich ViewModels in Jetpack Compose via lichViewModel.
@Composable
fun FooScreen(fooViewModel: FooViewModel = lichViewModel(FooViewModel)) {
// Use fooViewModel here.
}
AbstractViewModel is implementing
CoroutineScope
interface. This scope is bound to
Dispatchers.Main,
and will be cancelled just before AbstractViewModel.onCleared()
is called.
class BarViewModel(private val barRepository: BarRepository) : AbstractViewModel() {
val barText: MutableLiveData<String> = MutableLiveData("")
fun loadBarText() {
// The launched job will be automatically cancelled when this ViewModel is destroyed.
launch {
val barData = barRepository.loadBarData()
barText.value = barData.text
}
}
// snip...
}
This feature is almost equivalent to
Android Architecture Components' viewModelScope,
but you can simply write launch { ... }
instead of viewModelScope.launch { ... }
.
In unit tests, you can use mockViewModel(factory) { ... }
to mock ViewModels.
The following code is an example of mockViewModel
to mock the above BarViewModel
class using MockK:
@RunWith(AndroidJUnit4::class)
class BarActivityTest {
@After
fun tearDown() {
// You can omit this in Robolectric tests.
// All ViewModel mocks are automatically cleared for every Robolectric test.
clearAllMockViewModels()
}
@Test
fun testViewBinding() {
val mockBarText = MutableLiveData("Mocked.")
// Set mock ViewModel for `BarViewModel`.
val mockViewModelHandle = mockViewModel(BarViewModel) {
every { barText } returns mockBarText
}
ActivityScenario.launch(BarActivity::class.java).use { scenario ->
scenario.onActivity {
assertTrue(mockViewModelHandle.isCreated)
}
onView(withId(R.id.bar_text)).check(matches(withText("Mocked.")))
onView(withId(R.id.load_bar_button)).perform(click())
scenario.onActivity {
verify(exactly = 1) { mockViewModelHandle.mock.loadBarText() }
}
}
}
}
See also
MvvmSampleActivityTest
in the :sample-app:ui
module.
The mockViewModel function is also available for Mockito-Kotlin. Here is an example using Mockito-Kotlin.
@RunWith(AndroidJUnit4::class)
class BarActivityTest {
@After
fun tearDown() {
// You can omit this in Robolectric tests.
// All ViewModel mocks are automatically cleared for every Robolectric test.
clearAllMockViewModels()
}
@Test
fun testViewBinding() {
val mockBarText = MutableLiveData("Mocked.")
// Set mock ViewModel for `BarViewModel`.
val mockViewModelHandle = mockViewModel(BarViewModel) {
on { barText } doReturn mockBarText
}
ActivityScenario.launch(BarActivity::class.java).use { scenario ->
scenario.onActivity {
assertTrue(mockViewModelHandle.isCreated)
}
onView(withId(R.id.bar_text)).check(matches(withText("Mocked.")))
onView(withId(R.id.load_bar_button)).perform(click())
scenario.onActivity {
verify(mockViewModelHandle.mock, times(1)).loadBarText()
}
}
}
}
This library can be used in conjunction with Lich SavedState library.
The delegated properties and auto-generated ViewModelArgs
classes provided by the Lich SavedState
library can be used in Lich ViewModel library as well.
@GenerateArgs
class FooViewModel(savedStateHandle: SavedStateHandle) : AbstractViewModel() {
@Argument
private val userName: String by savedStateHandle.required()
@Argument(isOptional = true)
private val tags: Array<String> by savedStateHandle.initial(arrayOf("normal"))
@Argument
private var attachment: Parcelable? by savedStateHandle
@Argument
private val message: MutableLiveData<CharSequence> by savedStateHandle.liveData()
// snip...
companion object : ViewModelFactory<FooViewModel>() {
override fun createViewModel(context: Context, savedStateHandle: SavedStateHandle): FooViewModel =
FooViewModel(savedStateHandle)
}
}
class FooFragment : Fragment() {
private val fooViewModel by viewModel(FooViewModel)
// snip...
}
val fooFragment = FooFragment().also {
// This `FooViewModelArgs` is used to initialize the `savedStateHandle` of `FooFragment.fooViewModel`.
it.setViewModelArgs(FooViewModelArgs(userName = "John", attachment = null, message = "Hello."))
}
The generated ViewModelArgs
class can be used in the argument of the viewModel
extension as follows.
class FooFragment : Fragment() {
private val fooViewModel by viewModel(FooViewModel) {
// This `FooViewModelArgs` is used to initialize the `savedStateHandle` of `fooViewModel`.
FooViewModelArgs(userName = "John", attachment = null, message = "Hello.").toBundle()
}
// snip...
}
SampleViewModel
in the :sample-app:ui
module.