mirror of
https://github.com/bitwarden/android.git
synced 2024-12-18 07:11:51 +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(),
|
viewModel: RootNavViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
package com.x8bit.bitwarden.ui.feature.rootnav
|
package com.x8bit.bitwarden.ui.feature.rootnav
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.x8bit.bitwarden.ui.base.BaseViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -14,18 +11,20 @@ import javax.inject.Inject
|
||||||
* Manages root level navigation state of the application.
|
* Manages root level navigation state of the application.
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class RootNavViewModel @Inject constructor() : ViewModel() {
|
class RootNavViewModel @Inject constructor() :
|
||||||
|
BaseViewModel<RootNavState, Unit, Unit>(
|
||||||
private val _state = MutableStateFlow<RootNavState>(RootNavState.Splash)
|
initialState = RootNavState.Splash,
|
||||||
val state: StateFlow<RootNavState> = _state.asStateFlow()
|
) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
delay(1000)
|
delay(1000)
|
||||||
_state.value = RootNavState.Login
|
mutableStateFlow.value = RootNavState.Login
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: Unit) = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,13 +12,13 @@ class RootNavViewModelTests : BaseViewModelTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be splash`() {
|
fun `initial state should be splash`() {
|
||||||
val viewModel = RootNavViewModel()
|
val viewModel = RootNavViewModel()
|
||||||
assert(viewModel.state.value is RootNavState.Splash)
|
assert(viewModel.stateFlow.value is RootNavState.Splash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `state should move from splash to login`() = runTest {
|
fun `state should move from splash to login`() = runTest {
|
||||||
val viewModel = RootNavViewModel()
|
val viewModel = RootNavViewModel()
|
||||||
viewModel.state.test {
|
viewModel.stateFlow.test {
|
||||||
assert(awaitItem() is RootNavState.Splash)
|
assert(awaitItem() is RootNavState.Splash)
|
||||||
assert(awaitItem() is RootNavState.Login)
|
assert(awaitItem() is RootNavState.Login)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue