Skip to content

Instantly share code, notes, and snippets.

@oianmol
Created January 3, 2026 14:50
Show Gist options
  • Select an option

  • Save oianmol/5fca817851f8056a6a94401405200e79 to your computer and use it in GitHub Desktop.

Select an option

Save oianmol/5fca817851f8056a6a94401405200e79 to your computer and use it in GitHub Desktop.
Shared ViewModel with Atomic Design Ui Config.
/**
* 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