Created
January 3, 2026 14:50
-
-
Save oianmol/5fca817851f8056a6a94401405200e79 to your computer and use it in GitHub Desktop.
Shared ViewModel with Atomic Design Ui Config.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Example feature showing: | |
| * - Config-driven screen contract (Config + Event) | |
| * - ConfigProvider that builds the initial Config (wires callbacks → events) | |
| * - ViewModel implemented with a StateMachine (event → state) + use case call | |
| * | |
| * Names are intentionally generic (no product/company prefixes). | |
| */ | |
| import androidx.compose.runtime.Immutable | |
| import androidx.lifecycle.ViewModel | |
| import kotlinx.coroutines.CoroutineScope | |
| import kotlinx.coroutines.launch | |
| // ------------------------------ | |
| // 1) Screen Contract (shared) | |
| // ------------------------------ | |
| object SampleScreen { | |
| const val NAME = "SampleScreen" | |
| @Immutable | |
| sealed interface Event { | |
| data class OnInputChanged(val input: String) : Event | |
| data object OnSubmitClicked : Event | |
| } | |
| @Immutable | |
| data class Config( | |
| val inputField: InputFieldConfig?, | |
| val submitButton: ButtonConfig, | |
| val statusText: TextConfig?, | |
| ) | |
| // Minimal, platform-agnostic component configs | |
| @Immutable | |
| data class InputFieldConfig( | |
| val text: String, | |
| val contentDescription: String, | |
| val onTextChange: (String) -> Unit, | |
| ) | |
| @Immutable | |
| data class ButtonConfig( | |
| val title: String, | |
| val enabled: Boolean, | |
| val onClick: () -> Unit, | |
| ) | |
| @Immutable | |
| data class TextConfig(val text: String) | |
| // Factory helpers to build component configs | |
| fun inputField(text: String, onEvent: (Event) -> Unit) = InputFieldConfig( | |
| text = text, | |
| contentDescription = "sample_input", | |
| onTextChange = { onEvent(Event.OnInputChanged(it)) } | |
| ) | |
| fun submitButton(enabled: Boolean, onEvent: (Event) -> Unit) = ButtonConfig( | |
| title = "Submit", | |
| enabled = enabled, | |
| onClick = { onEvent(Event.OnSubmitClicked) } | |
| ) | |
| } | |
| // ------------------------------ | |
| // 2) Config Provider (shared) | |
| // ------------------------------ | |
| interface StateProvider<State, Event> { | |
| fun provide(onEvent: (Event) -> Unit): State | |
| } | |
| class SampleScreenConfigProvider : StateProvider<SampleScreen.Config, SampleScreen.Event> { | |
| override fun provide(onEvent: (SampleScreen.Event) -> Unit): SampleScreen.Config { | |
| val initialInput = "" | |
| return SampleScreen.Config( | |
| inputField = SampleScreen.inputField(text = initialInput, onEvent = onEvent), | |
| submitButton = SampleScreen.submitButton( | |
| enabled = initialInput.isNotBlank(), | |
| onEvent = onEvent | |
| ), | |
| statusText = null | |
| ) | |
| } | |
| } | |
| // ------------------------------ | |
| // 3) Use Case (shared) | |
| // ------------------------------ | |
| class SubmitSampleUseCase { | |
| suspend operator fun invoke(input: String): String { | |
| // Pretend we hit a repository/network and return a result string | |
| return "Submitted: $input" | |
| } | |
| } | |
| // ------------------------------ | |
| // 4) StateMachine (shared abstraction) | |
| // ------------------------------ | |
| // Assume your project already has something like this. | |
| // The key idea: mapEachEventToState { ... } and setState { ... }. | |
| interface StateMachine<Event, State> { | |
| val state: kotlinx.coroutines.flow.StateFlow<State> | |
| fun sendEvent(event: Event) | |
| fun setState(reducer: State.() -> State) | |
| fun mapEachEventToState(handler: suspend (Event) -> Unit) | |
| } | |
| // Example factory you likely already have in your codebase. | |
| class StateMachineWithProvider<Event, State>( | |
| initialStateProvider: StateProvider<State, Event>, | |
| private val coroutineScope: CoroutineScope, | |
| ) : StateMachine<Event, State> { | |
| private val _state = kotlinx.coroutines.flow.MutableStateFlow<State>( | |
| initialStateProvider.provide(::sendEvent) | |
| ) | |
| override val state = _state | |
| private val events = kotlinx.coroutines.flow.MutableSharedFlow<Event>(extraBufferCapacity = 64) | |
| init { | |
| coroutineScope.launch { | |
| events.collect { /* no-op until mapEachEventToState is called */ } | |
| } | |
| } | |
| override fun sendEvent(event: Event) { | |
| events.tryEmit(event) | |
| } | |
| override fun setState(reducer: State.() -> State) { | |
| _state.value = _state.value.reducer() | |
| } | |
| override fun mapEachEventToState(handler: suspend (Event) -> Unit) { | |
| coroutineScope.launch { | |
| events.collect { handler(it) } | |
| } | |
| } | |
| } | |
| // ------------------------------ | |
| // 5) ViewModel (shared) | |
| // ------------------------------ | |
| class SampleScreenViewModel( | |
| private val viewModelScope: CoroutineScope, | |
| private val configProvider: SampleScreenConfigProvider, | |
| private val submitUseCase: SubmitSampleUseCase, | |
| private val sm: StateMachine<SampleScreen.Event, SampleScreen.Config> = | |
| StateMachineWithProvider(initialStateProvider = configProvider, coroutineScope = viewModelScope) | |
| ) : ViewModel(), StateMachine<SampleScreen.Event, SampleScreen.Config> by sm { | |
| init { | |
| bindEvents() | |
| } | |
| private fun bindEvents() { | |
| mapEachEventToState { event -> | |
| when (event) { | |
| is SampleScreen.Event.OnInputChanged -> { | |
| setState { | |
| val updatedInput = event.input | |
| copy( | |
| inputField = SampleScreen.inputField(updatedInput, onEvent = ::sendEvent), | |
| submitButton = SampleScreen.submitButton( | |
| enabled = updatedInput.isNotBlank(), | |
| onEvent = ::sendEvent | |
| ), | |
| statusText = null | |
| ) | |
| } | |
| } | |
| SampleScreen.Event.OnSubmitClicked -> { | |
| // Capture current input safely from state | |
| val currentInput = state.value.inputField?.text.orEmpty() | |
| // Optimistic UI: disable button + show loading text | |
| setState { | |
| copy( | |
| submitButton = submitButton.copy(enabled = false), | |
| statusText = SampleScreen.TextConfig("Submitting…") | |
| ) | |
| } | |
| viewModelScope.launch { | |
| val resultText = submitUseCase(currentInput) | |
| setState { | |
| copy( | |
| statusText = SampleScreen.TextConfig(resultText), | |
| submitButton = submitButton.copy(enabled = currentInput.isNotBlank()) | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment