| name | add-feature |
| description | End-to-end workflow for adding a user-facing feature to TasKan — domain model, Room schema, repository, ViewModel, Compose screen, bilingual strings and tests, in the order that keeps the build green. Use when asked to add or extend a feature (tags, reminders, subtasks, widgets, sorting, a new view…), not for a one-line fix. |
Adding a feature to TasKan
Work outward from the domain. Each step compiles on its own, so a mistake surfaces next to the code
that caused it rather than 200 lines later.
1. Decide whether the schema changes
If the feature stores anything new, it does. Stop and run the room-migration skill first, then
come back — retrofitting a migration after the UI is written means redoing the entity, the DAO and
the tests.
If it stores nothing new (a filter, a sort, a view), skip to step 3.
2. Domain model
Add or extend the type in core/model. Keep it Android-free: no Context, no @Composable, no
androidx imports. Pure logic — matching, sorting, roll-ups — goes here too, as top-level functions,
because this is the layer that gets real unit tests.
Write those tests now, in app/src/test/java/com/taskan/core/model/. They run in about a second and
they are the cheapest place to discover that the idea does not quite work.
./gradlew :app:testDebugUnitTest
3. Data layer
- DAO — add queries to
data/local/dao/. Return Flow for anything the UI observes. Use
IS :param for nullable columns.
- Entity mapping — extend
toDomain() / toEntity() in the same file as the entity.
- Repository — add the method to the interface in
data/repository/ first, then implement it in
the Offline* class. The interface is what the ViewModel sees; keep it in domain types.
If the query is non-trivial — anything recursive, anything with GROUP BY — add a Robolectric test
in app/src/test/java/com/taskan/data/local/ alongside TaskDaoSubtreeTest. SQL is the one place
in this project where a plausible-looking query can be quietly wrong.
4. ViewModel
Extend the existing HomeViewModel / TasksViewModel, or add one under the relevant ui/ package.
- Expose one
StateFlow<XxxUiState> built with combine(...).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initial).
- Put one-shot things — snackbars, rejected actions — on the
Channel-backed eventFlow, not in
the state. State replays on rotation; a snackbar should not.
- Actions are plain functions returning
Job via viewModelScope.launch { … }.
5. Strings, then UI
Add the strings before writing the composable, to both res/values/strings.xml and
res/values-ja/strings.xml. Doing it in this order means the composable is written against real
resource ids and there is no "add the Japanese later" step to forget.
Then the composable, under the matching ui/ package. Follow the existing screens: state in,
callbacks out, MaterialTheme.colorScheme for every colour, stable keys on every list item.
Reuse components/TaskRow, components/FolderRow, components/EmptyState rather than writing a
new row — visual consistency across the two tabs is a feature.
6. Verify
./gradlew :app:testDebugUnitTest :app:assembleDebug
Then check by hand, because these are the things tests here do not catch:
- Dark theme, and dynamic colour on (Settings → Appearance).
- Japanese locale — the longest label, not the shortest.
- A folder nested at least three levels deep, and a task with no folder at all.
- Large font scale, if the feature adds text.
7. Write it down
If the feature changes an architectural rule, update CLAUDE.md or the relevant file in
.claude/rules/. If it lands something from docs/roadmap.md, tick it off there.