Implement category feature and wire DI foundation #1
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Overview
Foundation slice. Implements the
categoryfeature end-to-end and wires up DI +MainApplication. Everything else in the app depends on this.Working tree state (already done — do NOT redo)
The planning step for this slice is already complete in the working tree (uncommitted):
Vico,Room,PDFBox-Androidadded togradle/libs.versions.tomlandapp/build.gradle.kts.example_feature,data/repository,data/remoteremoved.expense,category,recurring,bankstatement, andpreference(incl.AppPreference,ExpensePreference).LocalDateConverter, 3 entities, 3 DAOs (with@Relationrows), 3 mappers,AppDatabase(v1).TODO(...)bodies.The default fallback category is named "Uncategorized" (not "Other") — this was confirmed after the planning step.
What to implement
1. Implement 4 category interactors (replace TODOs)
GetCategories.ktsubscribeAll()→dao.subscribeAll().map { rows -> rows.map { it.toModel() } }awaitOne(id)→dao.getById(id)?.toModel()awaitAll()→dao.getAll().map { it.toModel() }awaitDefault()→dao.getDefault()?.toModel() ?: error("Default category not found — SeedDefaultCategories must run on app start")UpsertCategory.ktawait(category):id == 0L:dao.upsert(category.toEntity())and return the returnedLong.id != 0L: look up the existing row. If the existing row hasisDefault = falseand the incoming hasisDefault = true, force the incoming to use the existingisDefault = falsebefore upserting (DB wins for theisDefaultflag). Otherwise upsert normally. Return the id.DeleteCategory.ktawait(id):idviadao.getById(id)?.toModel(). If null, return (nothing to delete).category.isDefault == true, throwIllegalArgumentException("Cannot delete a default category").getCategories.awaitDefault().id.reassignExpenseCategory.await(fromCategoryId = id, toCategoryId = fallbackId)— noteReassignExpenseCategoryis defined indomain/expense/interactor; if you haven't implemented the expense interactor yet, define a minimalReassignExpenseCategory(expenseDao)here that just runs the SQLUPDATE, OR import the full one (preferred). Either works as long as the interactor exists by the timeDeleteCategoryis wired in DI.dao.deleteById(id).SeedDefaultCategories.ktawait():dao.count() > 0, return (no-op).Category.Companion.DEFAULT_COLOR_*constants andisDefault = trueonly for Uncategorized. AlliconName = null.dao.upsertAll(defaults.map { it.toEntity() }).2. Wire DI
di/DataModule.ktdi/CoreModule.ktdi/PreferenceModule.kt(create new)di/DomainModule.kt(category factories only — leave rest empty for now)3. Create
MainApplicationand register in manifestui/base/MainApplication.ktNote:
PDFBoxResourceLoader.init(this)should be added once the bank-statement slice (#4) is implemented. For this slice, do not call it.AndroidManifest.xml— addandroid:name=".ui.base.MainApplication"to the<application>tag.4. Verify compilation
Run
./gradlew assembleDebug. Must succeed before this issue is done.Out of scope (belongs to other issues)
Acceptance
DataModule,CoreModule,PreferenceModule,DomainModulewiredMainApplicationexists and seeds default categories on first launchMainApplication./gradlew assembleDebugsucceedsImplementation rule
Per
AGENTS.md— do not start implementation without explicit user sign-off on this issue. When working, check for related issues in the remote repo first.