Skip to main content

Jetpack Compose and Compose Multiplatform

The module provides ContainerHost extensions for easy subscription from Composables.

caution

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()
}
)