mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
BIT-145: Add BaseViewModel and update existing ViewModels (#11)
This commit is contained in:
parent
ee199b9e9f
commit
2ff4912b92
4 changed files with 90 additions and 12 deletions
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue