#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