BIT-145: Add BaseViewModel and update existing ViewModels (#11)

This commit is contained in:
Brian Yencho 2023-08-28 09:37:05 -05:00 committed by Álison Fernandes
parent ee199b9e9f
commit 2ff4912b92
4 changed files with 90 additions and 12 deletions

View file

@ -0,0 +1,79 @@
package com.x8bit.bitwarden.ui.base
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
/**
* A base [ViewModel] that helps enforce the unidirectional data flow pattern and associated
* responsibilities of a typical ViewModel:
*
* - Maintaining and emitting a current state (of type [S]) with the given `initialState`.
* - Emitting one-shot events as needed (of type [E]). These should be rare and are typically
* reserved for things such as non-state based navigation.
* - Receiving actions (of type [A]) that may induce changes in the current state, trigger an
* event emission, or both.
*/
abstract class BaseViewModel<S, E, A>(
initialState: S,
) : ViewModel() {
protected val mutableStateFlow: MutableStateFlow<S> = MutableStateFlow(initialState)
protected val eventChannel: Channel<E> = Channel(capacity = Channel.UNLIMITED)
private val internalActionChannel: Channel<A> = Channel(capacity = Channel.UNLIMITED)
/**
* A [StateFlow] representing state updates.
*/
val stateFlow: StateFlow<S> = mutableStateFlow.asStateFlow()
/**
* A [Flow] of one-shot events. These may be received and consumed by only a single consumer.
* Any additional consumers will receive no events.
*/
val eventFlow: Flow<E> = eventChannel.receiveAsFlow()
/**
* A [SendChannel] for sending actions to the ViewModel for processing.
*/
val actionChannel: SendChannel<A> = internalActionChannel
init {
viewModelScope.launch {
internalActionChannel
.consumeAsFlow()
.collect { action ->
handleAction(action)
}
}
}
/**
* Handles the given [action] in a synchronous manner.
*
* Any changes to internal state that first require asynchronous work should post a follow-up
* action that may be used to then update the state synchronously.
*/
protected abstract fun handleAction(action: A): Unit
/**
* Helper method for sending an internal action.
*/
protected suspend fun sendAction(action: A) {
actionChannel.send(action)
}
/**
* Helper method for sending an event.
*/
protected fun sendEvent(event: E) {
viewModelScope.launch { eventChannel.send(event) }
}
}

View file

@ -21,7 +21,7 @@ fun RootNavScreen(
viewModel: RootNavViewModel = viewModel(),
) {
val navController = rememberNavController()
val state by viewModel.state.collectAsStateWithLifecycle()
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
NavHost(
navController = navController,

View file

@ -1,12 +1,9 @@
package com.x8bit.bitwarden.ui.feature.rootnav
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.ui.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -14,18 +11,20 @@ import javax.inject.Inject
* Manages root level navigation state of the application.
*/
@HiltViewModel
class RootNavViewModel @Inject constructor() : ViewModel() {
private val _state = MutableStateFlow<RootNavState>(RootNavState.Splash)
val state: StateFlow<RootNavState> = _state.asStateFlow()
class RootNavViewModel @Inject constructor() :
BaseViewModel<RootNavState, Unit, Unit>(
initialState = RootNavState.Splash,
) {
init {
viewModelScope.launch {
@Suppress("MagicNumber")
delay(1000)
_state.value = RootNavState.Login
mutableStateFlow.value = RootNavState.Login
}
}
override fun handleAction(action: Unit) = Unit
}
/**

View file

@ -12,13 +12,13 @@ class RootNavViewModelTests : BaseViewModelTest() {
@Test
fun `initial state should be splash`() {
val viewModel = RootNavViewModel()
assert(viewModel.state.value is RootNavState.Splash)
assert(viewModel.stateFlow.value is RootNavState.Splash)
}
@Test
fun `state should move from splash to login`() = runTest {
val viewModel = RootNavViewModel()
viewModel.state.test {
viewModel.stateFlow.test {
assert(awaitItem() is RootNavState.Splash)
assert(awaitItem() is RootNavState.Login)
}