#compose #mvvm #unidirectional #android
A unidirectional architecture consists of the following building blocks:
* State store
* Action to modify state or execute a side effect
* Side effect
## How it works together?
State store exposes a stream of state changes (could be `Flow`, `RxJava` , `LiveData` or `StateFlow` (the Compose one)). It's pretty trivial to turn that into a tree of Composables.
To perform some action (clicking on a button to confirm an operation, retrying) an `Action` is dispatched. It can change the state by reducing it from previous state:
```kotlin
fun reduce(state: ThisScreenState, action: Action): ThisScreenState
when (action) {
RetryAction -> state.copy(loading = true)
else -> ...
}
```
Or if the app needs to interact with an outside world (fetching from a remote service, persisting something, etc) an `Effect` can be triggered:
```kotlin
class AddToFavsEffect(service: BookService): Effect {
override fun observe(actions: Flow<Action>): Flow<Action> =
actions.filterIsInstance<AddToFavsAction>()
.doOnEach { service.addToFavs(it.id) }
.flatMapLatest { emptyFlow() }
}
```
## State store
One of the possible approaches is to use the `ViewModel` from Arch Components and make it host a `StateFlow` .
```kotlin
abstract class BaseViewModel<S>(initialState: S) : ViewModel() {
private val store = MutableStateFlow(initialState)
}
```
Actions can be dispatched to a `SharedFlow`:
```kotlin
private val actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
```
`ViewModel` will start collecting actions on `init` and reduce state on each action:
```kotlin
init {
actions.map { reduce(store.value, it) }
.onEach { store.value = it }
.launchIn(viewModelScope)
}
```
## Effects
To interact with an outside world, let's add side effects to our architecture.
An effect receives a stream of actions as an input and can return a stream of actions as an output. Emitted actions will be used to reduce a new state.
```kotlin
inteface Effect {
fun collect(actions: Flow<Action>): Flow<Action>
}
```
```kotlin
abstract class BaseViewModel<S>(effects: List<Effect>, initialState: S) : ViewModel() {
private val store = MutableStateFlow(initialState)
}
```
`ViewModel` will pass a flow of actions to each effect on `init`:
```kotlin
init {
effects.forEach { effect ->
effect.observe(actions)
.onEach { actions.tryEmit(it) }
.launchIn(viewModelScope)
}
}
```
This is a naive approach that will re-emit action to the effect if effect does not remove them from the stream.
## Consuming the state
`collectAsStateWithLifecycle` makes it really easy to observe and apply your state.
It will start producing the state when the lifecycle `STARTED` state and stop when it gets into `STOPPED` state.
```kotlin
@Composable
fun SomeScreen(
val viewModel: SomeViewModel,
) {
viewModel.observeState().collectAsStateWithLifecycle(initialValue = SomeState.Loading).value.let { state ->
when (state) {
is SomeState.Loading -> PortfolioLoading()
is SomeState.Loaded -> {
Data(...)
if (state.error != null) {
Error(...) {
viewModel.retry() // dispatches Retry action
}
}
}
}
}
}
```
## Closing notes
Unidirectional or redux-like architecture plays really nice with Compose by proving a single source of state. Any interactions with I/O, Android OS and other outside world components are scoped to side effects.
# More to cover
- Global app state
- Navigation
- Passing results between screens