| name | create-feature |
| description | Scaffolds a complete feature package — Model, View, and Presenter unit tests — following the project's Molecule + Koin architecture. Orchestrates the create-model, create-view, and create-model-test skills. Creates a module file only when third-party dependencies are needed. Use this when the user wants to add a new screen end-to-end. |
| metadata | {"author":"leandro","version":"1.0"} |
Create Feature
This skill scaffolds all the files needed for a new feature screen in one pass:
| File | Skill reference |
|---|
<FeatureName>Model.kt | create-model |
<FeatureName>Module.kt (optional) | this skill — only when third-party deps are needed |
<feature>/data/<Entity>.kt + <Feature>Repository.kt + impl + .sq (optional) | create-repository |
<FeatureName>View.kt | create-view |
<FeatureName>ModelPresenterTest.kt | create-model-test |
Navigation wiring in MainActivity.kt and the home-screen menu entry in ListView.kt are also handled here.
Step 1 — Gather Requirements
Before writing any code, clarify:
- Feature name — e.g.,
Weather, Profile, Settings
- Package — typically
com.gethomsefe.arch26.<featurename> (lowercase, no spaces)
- State & actions — what data does the screen display? What can the user do?
- Data layer — does the presenter need persisted or network data? If so, follow Step 2a before the Model.
- Effects / navigation — does the screen navigate away or return a value to the caller?
- Menu entry — should it appear in the home list (
ListView)?
Step 2a — Create the Repository (if needed)
If the feature requires a data layer (database, network, or any external source), follow the create-repository skill in full before writing the Model.
Key points:
- Create
app/src/main/java/com/gethomsefe/arch26/<feature>/data/ with the domain model, interface, and implementation.
- For SQLDelight: add a
.sq file under app/src/main/sqldelight/com/gethomesafe/arch26/ and run ./gradlew assembleDebug before proceeding.
- The
@Singleton implementation is discovered automatically — no extra module file unless the feature introduces new third-party dependencies.
- The Fake goes in the test file created in Step 7.
Skip this step entirely for features with only local UI state (counters, toggles, form inputs).
Step 2 — Create the Model
Follow the create-model skill in full.
Key points:
- Create
app/src/main/java/com/gethomsefe/arch26/<feature>/<FeatureName>Model.kt
- The
Presenter class must be annotated with @Factory — this is what Koin's @ComponentScan picks up.
- Use
sealed interface Action + perform lambda, or interface Actions, depending on complexity.
Step 3 — Create a Module File (only if needed)
The root Arch26Module carries @ComponentScan, which auto-discovers every @Factory, @Single, and @Singleton annotation in the entire com.gethomsefe.arch26 package tree. No per-feature @Module class is needed.
Presenter only (most common)
Nothing to do — the @Factory Presenter in the Model is discovered automatically.
Third-party dependencies (network / database / external SDK)
When the feature needs objects that can't carry Koin annotations directly (e.g., an HttpClient, a Database), create <FeatureName>Module.kt with plain top-level annotated functions. No @Module class required.
package com.gethomsefe.arch26.<feature>
import org.koin.core.annotation.Singleton
@Singleton
fun someClient(): SomeClient = SomeClient(...)
@Singleton
fun someOtherDep(client: SomeClient): SomeDep = SomeDep(client)
No changes to Arch26.kt are needed — the root @ComponentScan picks up annotated functions in any subpackage.
Annotation quick-reference
| Annotation | When to use |
|---|
@Factory | Short-lived dependency, new instance per injection |
@Single / @Singleton | Shared, long-lived dependency |
Step 4 — Create the View
Follow the create-view skill in full.
Key points:
- Create
app/src/main/java/com/gethomsefe/arch26/<feature>/<FeatureName>View.kt
- Two
Pane overloads: connected (with context(scope: CoroutineScope)) + stateless (takes State directly).
- Define a
sealed interface Effect inside the view object only if navigation or outbound events are needed.
retainMolecule keys must match any presenter constructor arguments.
Step 5 — Wire Navigation in MainActivity
Open MainActivity.kt (app/src/main/java/com/gethomsefe/arch26/MainActivity.kt).
a) Add a Route
@Serializable
sealed interface Route {
data object <FeatureName> : Route
data class <FeatureName>(val id: Int) : Route
}
b) Add a NavEntry in entryProvider
Route.<FeatureName> -> NavEntry(route) {
<FeatureName>View.Pane(Modifier.fillMaxSize())
}
With effects:
Route.<FeatureName> -> NavEntry(route) {
<FeatureName>View.Pane(
modifier = Modifier.fillMaxSize(),
produce = { effect ->
when (effect) {
<FeatureName>View.Effect.GoBack -> backStack.removeLastOrNull()
}
}
)
}
Step 6 — Add to the Home Menu (if requested)
ListView.kt — add an Effect case and a list item:
sealed interface Effect {
data object OnShow<FeatureName> : Effect
}
item {
ListItem(
headlineContent = { Text("<Feature Display Name>") },
modifier = Modifier.clickable { produce(Effect.OnShow<FeatureName>) }
)
}
MainActivity.kt — handle the new effect in the Route.List NavEntry:
Route.List -> NavEntry(route) {
ListView.Pane {
when (it) {
ListView.Effect.OnShow<FeatureName> -> backStack.add(Route.<FeatureName>)
}
}
}
Step 7 — Create the Tests
Follow the create-model-test skill in full.
Key points:
- Create
app/src/test/java/com/gethomsefe/arch26/<feature>/<FeatureName>ModelPresenterTest.kt
- Instantiate
Presenter() directly — no Koin needed.
- Use
moleculeFlow(RecompositionMode.Immediate) + Turbine.
- For async presenters (
rememberLoader/rememberWorker), use advanceTimeBy with @OptIn(ExperimentalCoroutinesApi::class).
- Cover: initial state, one test per meaningful action, async state transitions if applicable.
Run the tests after writing:
./gradlew :app:testDebugUnitTest --tests "*.<FeatureName>ModelPresenterTest"
Checklist
Work through each item in order. Do not move to the next until the current file compiles.
Complete Example — "Weather" feature (sketch)
Package: com.gethomsefe.arch26.weather
Files to create:
app/src/main/java/com/gethomsefe/arch26/weather/
WeatherModel.kt
WeatherView.kt
app/src/test/java/com/gethomsefe/arch26/weather/
WeatherModelPresenterTest.kt
No module file needed — WeatherModel.Presenter is @Factory and is discovered by the root @ComponentScan.
WeatherModel.kt (loads current conditions, lets user refresh):
package com.gethomsefe.arch26.weather
import androidx.compose.runtime.Composable
import com.gethomsefe.arch26.Busy
import com.gethomsefe.arch26.Loader
import com.gethomsefe.arch26.rememberLoader
import org.koin.core.annotation.Factory
object WeatherModel {
sealed interface Action {
data object Refresh : Action
}
data class State(
val conditions: Loader<String>,
val perform: (Action) -> Unit
)
@Factory
class Presenter {
@Composable
operator fun invoke(): State {
var conditions by rememberLoader { fetchConditions() }
return State(
conditions = conditions,
perform = { when (it) { Action.Refresh -> { conditions = Busy(Unit) } } }
)
}
private suspend fun fetchConditions(): String = "Sunny, 22°C"
}
}
WeatherView.kt (sketch — stateless Pane omitted for brevity):
package com.gethomsefe.arch26.weather
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.gethomsefe.arch26.retainMolecule
import kotlinx.coroutines.CoroutineScope
import org.koin.compose.koinInject
object WeatherView {
@Composable
context(scope: CoroutineScope)
fun Pane(modifier: Modifier = Modifier) {
val presenter = koinInject<WeatherModel.Presenter>()
val stateFlow = retainMolecule { presenter.invoke() }
val state by stateFlow.collectAsStateWithLifecycle()
Pane(modifier, state)
}
@Composable
fun Pane(modifier: Modifier, state: WeatherModel.State) {
}
}
Route + NavEntry in MainActivity.kt:
data object Weather : Route
Route.Weather -> NavEntry(route) {
WeatherView.Pane(Modifier.fillMaxSize())
}
ListView entry:
data object OnShowWeather : Effect
item {
ListItem(
headlineContent = { Text("Weather") },
modifier = Modifier.clickable { produce(Effect.OnShowWeather) }
)
}
ListView.Effect.OnShowWeather -> backStack.add(Route.Weather)