Create New Feature Screen
Before you dive in, it may be helpful to get familiar with the companion document Screen Structure and Wiring, which explains how the individual classes fit together. With that context in mind, this guide walks you through creating a new feature screen in the project. You can choose one of three paths:
- Using file templates — generate the screen files, then add the route and module dependencies
- Ask an AI agent — follow the repository's screen-creation skill
- Manual setup — when you need full control or when templates aren't available
Option 1: Using File Templates
- Ensure the UI Feature template is installed
- File → Manage IDE Settings → Import Settings...
- Import
file_templates.zipfrom the baselines-kmp root dir
- Right-click the destination package → New → UI Feature
- Enter the feature name (e.g., Profile, Settings)
- Keep the generated
UiModuleregistration: the current archive usesNavEntryFactory/entryand Metro ViewModel bindings, matching step 7 below. Adapt app-specific imports to your project's package, and ensure the generated screen preview imports your design system'sAppTheme. - Add the route and register the feature module as shown in steps 6 and 8 below.
Reimport the current archive if your IDE still generates NavGraphEntry / composable wiring.
Option 2: Setup via AI Agent
- Open chat with your AI assistant and point it to
AGENTS.mdand.agents/skills/create-screen/SKILL.md. - Ask to create new feature screen
- Provide all the necessary info requested by the AI agent
- Done 🎉 — your feature is wired into the app
Option 3: Manual Setup
- Create
*UiEvent
kotlin1sealed interface ProfileUiEvent : UiEvent {2 data object PerformLogout : ProfileUiEvent {3 override val dispatchPolicy = UiEventDispatchPolicy.ThrottleFirst()4 }5}
Use ThrottleFirst for click-like one-shot events. Use DebounceLatest only for delayed latest-value effects such as search or filtering. If a text field is controlled by ViewModel state, keep the visible input state immediate and debounce a separate effect event instead of the input-state event itself.
- Create
*UiState
kotlin1@Immutable2data class ProfileUiState(3 override val eventSink: (ProfileUiEvent) -> Unit,4) : UiState<ProfileUiEvent>
- Create
*ViewModel
Use the standard Metro map contribution below when all dependencies come from the app graph.
It does not require a separate @Inject annotation. Use @AssistedInject and an assisted factory only
for runtime arguments, as shown by PlaygroundViewModel.
kotlin1@ViewModelKey2@ContributesIntoMap(AppScope::class, binding<ViewModel>())3class ProfileViewModel : ViewModel(), Mvvm<ProfileUiEvent, ProfileUiState> {45 private val eventSink = createEventSink(::handleEvent)67 @Composable8 override fun state() = ProfileUiState(9 eventSink = eventSink,10 )1112 private fun handleEvent(event: ProfileUiEvent) {13 when (event) {14 ProfileUiEvent.PerformLogout -> handleLogout()15 }16 }1718 private fun handleLogout() {19 /* handle action */20 }21}
- Create
*Screen
kotlin1@Composable2fun ProfileScreen(onLogoutClicked: () -> Unit) {3 /* UI */4}
- Create
*Route
kotlin1@Composable2fun ProfileRoute(viewModel: ProfileViewModel) {3 val state = viewModel.state()4 val eventSink = state.eventSink5 ProfileScreen(6 onLogoutClicked = { eventSink(ProfileUiEvent.PerformLogout) }7 )8}
- Add the destination to the existing
AppNavRoutesobject inui/navigation
kotlin1@Serializable2data object Profile : NavRoute
Use kotlinx.serialization.Serializable and the app's io.baselines.sample.ui.navigation.NavRoute.
Keep the existing routes and AppNavRoutes.Default. Change GetStartRoute only if this screen should
be the app's start destination.
- Add DI
*Module
kotlin1import dev.zacsweers.metro.ContributesTo2import dev.zacsweers.metro.IntoSet3import dev.zacsweers.metro.Provides4import dev.zacsweers.metrox.viewmodel.metroViewModel5import io.baselines.sample.ui.navigation.AppNavRoutes6import io.baselines.toolkit.di.UiScope7import io.baselines.toolkit.navigation.NavEntryFactory89@ContributesTo(UiScope::class)10interface ProfileUiModule {1112 @Provides13 @IntoSet14 fun provideProfileNavEntryFactory(): NavEntryFactory = {15 entry<AppNavRoutes.Profile> {16 ProfileRoute(metroViewModel())17 }18 }19}
- Register the feature module
If this is a new module, add include(":ui:profile") to settings.gradle.kts and
api(projects.ui.profile) to commonMain.dependencies in app/multiplatform/build.gradle.kts.
The feature module should apply the Baselines Compose and DI plugins and depend on
projects.ui.navigation, projects.ui.viewModel, and projects.ui.designSystem as needed.
See Create New Module for build setup.
- Navigate from a ViewModel
Inject io.baselines.toolkit.navigation.Navigator into the ViewModel handling the action:
kotlin1navigator.navigate(AppNavRoutes.Profile)2// From a screen with a back action:3navigator.navigateBack()
These commands are synchronous and do not require a coroutine. The Navigation Guide covers stack replacement and parameterized routes.