Quick start¶
This builds a two screen app: a list that navigates to a detail screen. Every piece is shown in full, in the order you would write it.
1. Add the dependencies¶
Orbit needs the kotlinx-serialization plugin in any module that declares screens:
2. Declare the screens¶
Screens are the routing keys. They are @Serializable so the back stack can be written to disk
and rebuilt after process death.
@Serializable
data object HomeScreen : Screen
@Serializable
data class DetailScreen(val id: String) : Screen
Keep screens small — an id, not a whole model. The presenter loads the rest.
3. Declare state and events¶
State is what the UI draws. Events are what the UI sends back. The link between them is an
eventSink property on the state, which is why a UI never needs a reference to its presenter.
data class HomeState(
val items: ImmutableList<String>,
val eventSink: (HomeEvent) -> Unit,
) : OrbitUiState
sealed interface HomeEvent : OrbitUiEvent {
data class OpenDetail(val id: String) : HomeEvent
}
Use an immutable list
ImmutableList from kotlinx-collections-immutable lets Compose treat the state as stable and
skip recomposition when it has not changed. A plain List is not considered stable.
4. Write the presenter¶
A presenter is a @Composable returning state. Anything legal in a Composable is legal here:
remember, LaunchedEffect, collectAsState.
class HomePresenter(private val navigator: Navigator) : Presenter<HomeState> {
@Composable
override fun present(): HomeState {
val items = remember { List(30) { "Item ${it + 1}" }.toImmutableList() }
return HomeState(items) { event ->
when (event) {
is HomeEvent.OpenDetail -> navigator.goTo(DetailScreen(event.id))
}
}
}
}
5. Write the UI¶
The UI takes state and a Modifier, and nothing else.
@Composable
fun HomeContent(state: HomeState, modifier: Modifier = Modifier) {
LazyColumn(modifier.fillMaxSize()) {
items(state.items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.clickable { state.eventSink(HomeEvent.OpenDetail(item)) }
.padding(16.dp),
)
}
}
}
6. Register everything¶
Orbit is the registry that maps screens to presenters and UIs. Factories run in registration
order and the first non-null result wins; returning null means "not mine, try the next one".
fun buildOrbit(): Orbit = Orbit.Builder()
.addPresenterFactory { screen, navigator, _ ->
when (screen) {
is HomeScreen -> HomePresenter(navigator)
is DetailScreen -> DetailPresenter(screen, navigator)
else -> null
}
}
.addUiFactory { screen, _ ->
when (screen) {
is HomeScreen -> ui<HomeState> { state, modifier -> HomeContent(state, modifier) }
is DetailScreen -> ui<DetailState> { state, modifier -> DetailContent(state, modifier) }
else -> null
}
}
.setScreenSerializers {
subclass(HomeScreen::class)
subclass(DetailScreen::class)
}
.build()
Register every screen
A screen missing from setScreenSerializers compiles fine and fails at runtime the moment
the back stack is saved. This is the trade Orbit makes by not using Parcelable. Add the
subclass line at the same time you add the screen.
7. Host it¶
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val backStack = rememberSaveableBackStack(HomeScreen, AppJson)
val orbit = remember(backStack) { buildOrbit() }
ProvideOrbit(orbit) {
val navigator = rememberNavigator(backStack) { finish() }
BackHandler(enabled = !backStack.isAtRoot) { navigator.pop() }
NavigableOrbitContent(
navigator = navigator,
backStack = backStack,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
AppJson is the Json instance carrying your screen serializers:
val AppJson = Json {
serializersModule = SerializersModule {
polymorphic(Screen::class) {
subclass(HomeScreen::class)
subclass(DetailScreen::class)
}
}
}
The back stack is created before the Orbit instance because rememberSaveableBackStack needs
serializers, not the registry. If you would rather create it inside ProvideOrbit, use the
overload that takes the Orbit instead of a Json.
rememberNavigator's second argument runs when a pop happens at the root — on Android, finish
the activity.
What you get for free¶
- Back and forward navigation with a slide transition.
- Back stack survival across rotation and process death.
- Per screen state scoping, so pushing the same screen twice gives two independent instances.
Next¶
- Retained state — surviving rotation without a ViewModel.
- Overlays — dialogs that return a value.
- Testing — presenter tests with no emulator.