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:

  1. Using file templates — generate the screen files, then add the route and module dependencies
  2. Ask an AI agent — follow the repository's screen-creation skill
  3. Manual setup — when you need full control or when templates aren't available

Option 1: Using File Templates

  1. Ensure the UI Feature template is installed
    • File → Manage IDE Settings → Import Settings...
    • Import file_templates.zip from the baselines-kmp root dir
  2. Right-click the destination package → New → UI Feature
  3. Enter the feature name (e.g., Profile, Settings)
  4. Keep the generated UiModule registration: the current archive uses NavEntryFactory / entry and 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's AppTheme.
  5. 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

  1. Open chat with your AI assistant and point it to AGENTS.md and .agents/skills/create-screen/SKILL.md.
  2. Ask to create new feature screen
  3. Provide all the necessary info requested by the AI agent
  4. Done 🎉 — your feature is wired into the app

Option 3: Manual Setup

  1. Create *UiEvent
kotlin
1sealed 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.

  1. Create *UiState
kotlin
1@Immutable
2data class ProfileUiState(
3 override val eventSink: (ProfileUiEvent) -> Unit,
4) : UiState<ProfileUiEvent>
  1. 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.

kotlin
1@ViewModelKey
2@ContributesIntoMap(AppScope::class, binding<ViewModel>())
3class ProfileViewModel : ViewModel(), Mvvm<ProfileUiEvent, ProfileUiState> {
4
5 private val eventSink = createEventSink(::handleEvent)
6
7 @Composable
8 override fun state() = ProfileUiState(
9 eventSink = eventSink,
10 )
11
12 private fun handleEvent(event: ProfileUiEvent) {
13 when (event) {
14 ProfileUiEvent.PerformLogout -> handleLogout()
15 }
16 }
17
18 private fun handleLogout() {
19 /* handle action */
20 }
21}
  1. Create *Screen
kotlin
1@Composable
2fun ProfileScreen(onLogoutClicked: () -> Unit) {
3 /* UI */
4}
  1. Create *Route
kotlin
1@Composable
2fun ProfileRoute(viewModel: ProfileViewModel) {
3 val state = viewModel.state()
4 val eventSink = state.eventSink
5 ProfileScreen(
6 onLogoutClicked = { eventSink(ProfileUiEvent.PerformLogout) }
7 )
8}
  1. Add the destination to the existing AppNavRoutes object in ui/navigation
kotlin
1@Serializable
2data 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.

  1. Add DI *Module
kotlin
1import dev.zacsweers.metro.ContributesTo
2import dev.zacsweers.metro.IntoSet
3import dev.zacsweers.metro.Provides
4import dev.zacsweers.metrox.viewmodel.metroViewModel
5import io.baselines.sample.ui.navigation.AppNavRoutes
6import io.baselines.toolkit.di.UiScope
7import io.baselines.toolkit.navigation.NavEntryFactory
8
9@ContributesTo(UiScope::class)
10interface ProfileUiModule {
11
12 @Provides
13 @IntoSet
14 fun provideProfileNavEntryFactory(): NavEntryFactory = {
15 entry<AppNavRoutes.Profile> {
16 ProfileRoute(metroViewModel())
17 }
18 }
19}
  1. 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.

  1. Navigate from a ViewModel

Inject io.baselines.toolkit.navigation.Navigator into the ViewModel handling the action:

kotlin
1navigator.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.