Jetpack Compose and Compose Multiplatform
The module provides ContainerHost
extensions for easy subscription from Composables.
Compose Multiplatform support added in Orbit v10.0.0.
Including the module
implementation("org.orbit-mvi:orbit-compose:10.0.0")
Subscribing to a ContainerHost in Compose
Use the method below to subscribe to a ContainerHost in Compose.
The functions safely follow the Composable lifecycle and will automatically
subscribe only if the view is at least STARTED.
@Composable
fun SomeScreen(viewModel: SomeViewModel) {
val state by viewModel.collectAsState()
viewModel.collectSideEffect {
when(it) {
...
}
}
SomeContent(
state = state
)
}
TextField state hoisting
When dealing with TextField in Compose we often want the ViewModel to be the
owner of the state so we can handle validation and/or other logic such as
autocomplete suggestions.
It may seem natural to use TextField.onChangeValue to process the input
through an intent, with state emitting an updated value to
TextField.value, however, threading in TextField means user input is
partially lost.
Alternatively, TextField can be provided a TextFieldState which we can
provide and observe in our ViewModel:
class TextViewModel : ViewModel(), ContainerHost<TextViewModel.State, Nothing> {
override val container: Container<State, Nothing> = container(State()) {
coroutineScope {
launch {
snapshotFlow { state.textFieldState.text }.collectLatest { text ->
reduce { state.copy(isValid = text.isValid()) }
}
}
}
}
data class State(
val textFieldState: TextFieldState = TextFieldState(""),
val isValid: Boolean = false,
)
companion object {
fun CharSequence.isValid(): Boolean {
return this.isNotBlank() && this.length <= 10
}
}
}
Compose UI Testing
For better testability, separate the ViewModel from your UI. As shown above,
access the ViewModel only in SomeScreen, passing its state to SomeContent
for rendering. This makes it easy to test SomeContent in isolation, without
concerns about state conflation from the ViewModel.
Similarly, ViewModel callbacks can be passed into SomeContent, either
directly, or through an interface.
SomeContent(
state = state,
onSubmit = { viewModel.submit() }
)
// or
interface SomeCallbacks {
fun onSubmit()
}
SomeContent(
state = state,
callbacks = object : SomeCallbacks {
override fun onSubmit() = viewModel.submit()
}
)