| name | arcana-android-developer-skill |
| description | Android development guide based on Arcana Android enterprise architecture. Provides comprehensive support for Clean Architecture, Offline-First design, Jetpack Compose, Hilt DI, and MVVM Input/Output pattern. Suitable for Android project development, architecture design, code review, and debugging. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
Android Developer Skill
Professional Android development skill based on Arcana Android enterprise architecture.
โก Workflow โ Always Start From the Reference Project
Every task starts by cloning the complete reference project โ NEVER scaffold a new Android project from scratch:
git clone https://github.com/jrjohn/arcana-android.git [new-project-directory]
- Clone the reference project (command above).
- Build + test the UNTOUCHED clone first to establish a green baseline (
./gradlew clean build then ./gradlew test) before changing anything.
- Follow 0. Project Setup to rename the project/package and strip the demo screens โ while KEEPING the infrastructure: auth/security layers (encrypted SharedPreferences token storage), caching (
core/cache), offline/sync (sync/ + Room AppDatabase), the DI container (di/ Hilt modules), and deployment/build configs (Gradle setup, gradle/libs.versions.toml).
- Add features by copying the nearest working feature, then adapting (see ๐ Adding a feature) โ the recipe below is the completeness checklist. (File-by-File Feature Recipe)
๐ Adding a feature = copy the nearest working feature (NOT from scratch)
When you add a new feature/screen, do NOT re-create it from memory by walking the File-by-File Recipe on a blank slate. Copy the nearest already-working feature in the cloned reference, then adapt it.
- Find the closest conformant feature already in the reference โ e.g. the example Orders list/detail feature, which already demonstrates the full Model โ Repository โ Service/UseCase โ ViewModel (Input/Output/Effect) โ View chain wired through DI.
- Duplicate its ENTIRE file set (domain model, repository interface, service, Room entity, DAO + AppDatabase registration, DTO + API, repository implementation, mock repository, DI registration, ViewModel, screens, route + NavGraph, unit tests, UI tests) 1:1, keeping every layer.
- Rename + adapt to the new domain (types, navigation, endpoints, DI bindings).
- Diff against the original to confirm nothing was dropped โ same layer split, same ViewModel/use-case/repository boundary, same error model, same tests.
Why this is mandatory: the File-by-File Recipe is a checklist of what must exist, not a from-scratch build order. Re-deriving the pattern each time makes every step a chance to skip a layer (the ViewModel, the repository), wire a shortcut (view โ data/API directly), or drop the tests โ the "vibe-coding" deviations that compile and pass coverage but fail architecture review. Copying a known-good feature carries conformance in by construction; the recipe is then only your verification that nothing is missing.
Supporting files โ load on demand
| File | When to read |
|---|
reference.md | Deep-dive architecture reference โ layer responsibilities and project conventions |
patterns.md | Extended code patterns beyond those inlined in this file |
patterns/mvvm-input-output.md | Detailed MVVM Input/Output ViewModel walkthrough |
examples.md | Complete end-to-end feature examples to copy from |
checklists/production-ready.md | Pre-release checklist before declaring a feature done |
verification/commands.md | Full verification command set (superset of Quick Verification below) |
File-by-File Feature Recipe
Ordered file-by-file recipe for adding a complete feature (example: Orders) through all layers. Create files in this order โ each step compiles against the previous ones. Paths are relative to app/src/main/java/<your/package>/.
- Domain model โ
domain/model/Order.kt
โ Immutable data class, all fields from Spec.
- Repository interface โ
domain/repository/OrderRepository.kt
โ suspend fun methods returning Result<T> / Flow<T> of Domain models.
- Service โ
domain/service/OrderService.kt
โ Business rules and validation; only calls methods that exist on the repository interface.
- Room entity โ
data/local/entity/OrderEntity.kt
โ With toDomain() / toEntity() mapping and syncStatus field.
- DAO โ
data/local/dao/OrderDao.kt โ then register the entity + DAO in data/local/AppDatabase.kt.
- DTO + API โ
data/remote/dto/OrderDto.kt, data/remote/api/OrderApi.kt.
- Repository implementation โ
data/repository/OrderRepositoryImpl.kt
โ Offline-first: Room as single source of truth, schedule background sync.
- Mock repository โ
data/repository/mock/MockOrderRepository.kt
โ NEVER return emptyList()/null; 5-10 varied items, delay() latency, IDs consistent with other repositories.
- DI registration โ
di/RepositoryModule.kt โ @Binds the mock (development) or real implementation.
- ViewModel โ
ui/screens/orders/OrderListViewModel.kt
โ @HiltViewModel, Input/Output pattern + Effect channel.
- Screen โ
ui/screens/orders/OrderListScreen.kt (+ OrderDetailScreen.kt)
โ Loading/Error/Empty/Content states; stateless content composable.
- Route โ
nav/NavRoutes.kt โ add Orders / OrderDetail route objects.
- NavGraph โ
nav/NavGraph.kt โ composable() for each route; wire ALL onNavigate* callbacks (no default = {} left unwired).
- Unit tests โ
app/src/test/.../ui/screens/orders/OrderListViewModelTest.kt, app/src/test/.../data/repository/OrderRepositoryTest.kt.
- UI tests โ
app/src/androidTest/.../ui/screens/OrderListScreenTest.kt.
Then run the Quick Verification Commands โ route count must match composable count, and no empty mock lists may remain.
Core Architecture Principles
Clean Architecture - Three Layers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Presentation Layer โ
โ Compose UI + MVVM + Input/Output โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Domain Layer โ
โ Business Logic + Services + Models โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Data Layer โ
โ Offline-First Repository + Room + API โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Dependency Rules
- Unidirectional Dependencies: Presentation โ Domain โ Data
- Interface Segregation: Decouple layers through interfaces
- Dependency Inversion: Data layer implements Domain layer interfaces
๐ Quick Reference Card
When Creating New Screen:
1. [ ] Add route to NavRoutes.kt
2. [ ] Add composable to NavGraph.kt
3. [ ] Create ViewModel with Input/Output pattern
4. [ ] Implement Loading/Error/Empty states
5. [ ] Add navigation wiring (back, forward)
6. [ ] Verify mock data is non-empty
When Creating New Repository:
1. [ ] Define interface in domain/repository/
2. [ ] Implement in data/repository/
3. [ ] Register in Hilt module (@Binds)
4. [ ] Add mock data (NEVER empty!)
5. [ ] Verify ID consistency with other repositories
When Creating New Feature:
1. [ ] Check Spec for all related screens
2. [ ] Apply Spec Gap Prediction (ListโDetail, CreateโEdit)
3. [ ] Implement all UI states (Loading/Error/Empty/Success)
4. [ ] Run verification commands before PR
Quick Diagnosis:
| Symptom | Likely Cause | Check Command |
|---|
| Blank screen | Empty mock data | grep "emptyList()" *RepositoryImpl.kt |
| Navigation crash | Missing composable | grep "NavRoutes\." NavGraph.kt |
| Data not loading | ID mismatch | grep "id = \"" *RepositoryImpl.kt |
| Click does nothing | Empty handler | grep "onClick = { }" *.kt |
๐ฆ Rules Priority
๐ด CRITICAL (Must follow, violations cause Bug/Crash)
- Zero-Null Policy - Repository stubs must not return null/empty
- Navigation Wiring - All NavRoutes must have composable destinations
- ID Consistency - IDs must be consistent across Repositories
- Onboarding Flow - Must check Onboarding status after Register/Login
๐ก IMPORTANT (Strongly recommended, affects quality)
- UI State Handling - Loading/Error/Empty states
- Mock Data Quality - Use realistic mock data
- MVVM Input/Output - Follow standard pattern
- Offline-First - Local-first strategy
๐ข RECOMMENDED (Suggested, improves UX)
- Animation Standards - Transition animations
- Accessibility - Accessibility support
- Pull-to-Refresh - List pull-to-refresh
- Skeleton Loading - Skeleton screen loading
๐จ Error Handling Pattern
Unified Error Model
sealed class AppError {
data class Network(
val code: Int,
val message: String
) : AppError()
data class Validation(
val field: String,
val reason: String
) : AppError()
sealed class Auth : AppError() {
object SessionExpired : Auth()
object InvalidCredentials : Auth()
object Unauthorized : Auth()
}
data class NotFound(val resource: String) : AppError()
data class Unknown(val cause: Throwable? = null) : AppError()
}
Error Flow
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ Repository โ โ โ ViewModel โ โ โ UiState โ โ โ Screen โ
โ Result<T> โ โ Handle Err โ โ .Error โ โ ErrorUI โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
Repository Layer
suspend fun getData(): Result<Data> {
return try {
val response = api.getData()
if (response.isSuccessful) {
Result.success(response.body()!!)
} else {
Result.failure(AppError.Network(response.code(), response.message()))
}
} catch (e: IOException) {
Result.failure(AppError.Network(-1, "Network unavailable"))
} catch (e: Exception) {
Result.failure(AppError.Unknown(e))
}
}
ViewModel Layer
private fun handleResult(result: Result<Data>) {
result.fold(
onSuccess = { data ->
_output.update { it.copy(isLoading = false, data = data) }
},
onFailure = { error ->
val message = when (error) {
is AppError.Network -> "Network connection failed, please try again later"
is AppError.Auth.SessionExpired -> "Session expired, please login again"
is AppError.NotFound -> "Data not found"
else -> "An unknown error occurred"
}
_output.update { it.copy(isLoading = false, error = message) }
if (error is AppError.Auth.SessionExpired) {
_effect.emit(Effect.NavigateToLogin)
}
}
)
}
Screen Layer
@Composable
fun DataScreen(viewModel: DataViewModel = hiltViewModel()) {
val output by viewModel.output.collectAsStateWithLifecycle()
when {
output.isLoading -> LoadingState()
output.error != null -> ErrorState(
message = output.error!!,
onRetry = { viewModel.onInput(Input.Retry) }
)
output.data != null -> DataContent(output.data!!)
else -> EmptyState()
}
}
@Composable
private fun ErrorState(message: String, onRetry: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(Icons.Default.Error, null, tint = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Text(message, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onRetry) {
Text("Retry")
}
}
}
Global Error Handling
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Logger.e("Unhandled exception", throwable)
_globalError.value = "An error occurred, please try again later"
}
fun launchSafely(block: suspend () -> Unit) {
viewModelScope.launch(exceptionHandler) {
block()
}
}
}
๐งช Test Coverage Targets
Coverage Goals by Layer
| Layer | Target | Focus Areas |
|---|
| Domain (UseCase/Service) | 90%+ | Business logic, edge cases |
| Data (Repository) | 80%+ | Data transformation, error handling |
| Presentation (ViewModel) | 85%+ | State management, input handling |
| UI (Compose) | 60%+ | Critical user flows |
Test Types Required
Test Naming Convention
@Test
fun `methodName should expectedBehavior when condition`() {
}
fun `login should emit success when credentials are valid`()
fun `loadData should emit error when network fails`()
fun `submitForm should show validation error when email is invalid`()
Minimum Tests Before PR
./gradlew test
./gradlew connectedAndroidTest
./gradlew jacocoTestReport
MIN_COVERAGE=70
ACTUAL=$(cat build/reports/jacoco/test/html/index.html | grep -oP 'Total.*?\K\d+(?=%)')
if [ "$ACTUAL" -lt "$MIN_COVERAGE" ]; then
echo "โ Coverage $ACTUAL% < $MIN_COVERAGE% required"
exit 1
fi
Instructions
When handling Android development tasks, follow these principles:
Quick Verification Commands
Use these commands to quickly check for common issues:
grep -rn "NotImplementedError\|Result.failure.*Not.*implement" app/src/main/java/
grep -rn "onClick\s*=\s*{\s*}\|onClick\s*=\s*{.*TODO" app/src/main/java/
echo "Routes defined:" && grep -c "data object" app/src/main/java/**/nav/NavRoutes.kt
echo "Composables registered:" && grep -c "composable(NavRoutes\." app/src/main/java/**/nav/*NavGraph.kt
./gradlew :app:compileDebugKotlin
grep -rn "PlaceholderScreen\|ๅณๅฐๆจๅบ\|Coming Soon" app/src/main/java/
./gradlew test
grep -rn "onNavigate[A-Za-z]*:\s*(\s*)\s*->\s*Unit\s*=\s*{}" app/src/main/java/**/ui/screens/
echo "=== Screen Navigation Callbacks ===" && \
grep -rh "onNavigateTo[A-Za-z]*:" app/src/main/java/**/ui/screens/*.kt | grep -oE "onNavigateTo[A-Za-z]+" | sort -u
echo "=== NavGraph Wired Callbacks ===" && \
grep -rh "onNavigateTo[A-Za-z]*\s*=" app/src/main/java/**/nav/*NavGraph.kt | grep -oE "onNavigateTo[A-Za-z]+" | sort -u
echo "=== Repository Methods Called in Services ===" && \
grep -roh "repository\.[a-zA-Z]*(" app/src/main/java/**/domain/service/*.kt | sort -u
echo "=== Repository Methods Defined ===" && \
grep -rh "suspend fun [a-zA-Z]*(\|fun [a-zA-Z]*(" app/src/main/java/**/domain/repository/*.kt | grep -oE "fun [a-zA-Z]+\(" | sort -u
echo "=== Repository Interface Methods ===" && \
grep -rh "suspend fun\|fun " app/src/main/java/**/domain/repository/*Repository.kt | grep -oE "fun [a-zA-Z]+" | sort -u
echo "=== Repository Implementation Methods ===" && \
grep -rh "override.*suspend fun\|override.*fun " app/src/main/java/**/data/repository/*RepositoryImpl.kt | grep -oE "fun [a-zA-Z]+" | sort -u
grep -rn "ๅฐ็ก\|ๆซ็ก\|No.*data\|Empty\|ๅณๅฐๆจๅบ" app/src/main/java/**/ui/
grep -rn -B2 -A5 "Tab(" app/src/main/java/**/ui/screens/ | grep -E "Tab\(|when.*selectedTab|HorizontalPager"
grep -rn "Canvas\|Chart\|Graph\|drawLine\|drawRect" app/src/main/java/**/ui/ || echo "โ ๏ธ No charts found"
grep -rn "LottieAnimation\|rememberLottieComposition\|animate" app/src/main/java/**/ui/ || echo "โ ๏ธ No animations found - verify if required by Spec"
grep -rn 'Text(.*fontSize.*=.*[4-9][0-9]\.sp)' app/src/main/java/**/ui/screens/ && echo "โ ๏ธ Found large text - verify not placeholder for graphics"
grep -rn '"Test\|"Item \|"Example\|"User \|lorem\|ipsum' app/src/main/java/ && echo "โ ๏ธ Found generic test data"
โ ๏ธ CRITICAL: Route count MUST equal Composable count. If not, you have missing navigation destinations that will cause runtime crashes.
๐จ ABSOLUTE REQUIREMENT:
- PlaceholderScreen check MUST return empty
- "ๅณๅฐๆจๅบ" / "Coming Soon" MUST NOT exist in production code
- ALL tests MUST pass (not just be written)
- ALL Screen navigation callbacks MUST be wired in NavGraph (not using default empty
= {})
- Screen callbacks list MUST match NavGraph wired callbacks list
- ALL Repository methods called by Services MUST exist in Repository interfaces
- ALL Repository interface methods MUST have implementations in RepositoryImpl
If any of these return results or counts don't match, FIX THEM before completing the task.
๐ฆ User Journey Flow Verification (PROACTIVE)
The Problem
Navigation issues like "Onboarding skipped after login" are only discovered when users test the app manually. This section enables proactive detection of incomplete user flows.
User Flow Checkpoint System
CRITICAL: Before completing any feature, verify ALL user journeys are complete.
echo "=== User Journey Flow Verification ===" && \
echo ""
echo "--- First-Time User Flow ---" && \
echo "Expected: Splash โ Login/Register โ Onboarding โ Dashboard" && \
REGISTER_TARGET=$(grep -A5 "onRegisterSuccess" app/src/main/java/**/nav/*NavGraph.kt | grep "navigate(" | head -1) && \
echo "Register navigates to: $REGISTER_TARGET" && \
if echo "$REGISTER_TARGET" | grep -q "Dashboard"; then \
echo "โ ๏ธ WARNING: Register goes directly to Dashboard - Onboarding may be skipped!"; \
fi
echo "" && echo "--- Returning User Flow ---" && \
echo "Expected: Splash โ (check session) โ Dashboard OR Login" && \
LOGIN_TARGET=$(grep -A5 "onLoginSuccess" app/src/main/java/**/nav/*NavGraph.kt | grep "navigate(" | head -1) && \
echo "Login navigates to: $LOGIN_TARGET"
echo "" && echo "--- Onboarding Registration ---" && \
ONBOARDING_ROUTE=$(grep "Onboarding" app/src/main/java/**/nav/NavRoutes.kt) && \
ONBOARDING_COMPOSABLE=$(grep "NavRoutes.Onboarding" app/src/main/java/**/nav/*NavGraph.kt) && \
if [ -z "$ONBOARDING_ROUTE" ]; then \
echo "โ MISSING: Onboarding route not defined in NavRoutes"; \
elif [ -z "$ONBOARDING_COMPOSABLE" ]; then \
echo "โ MISSING: Onboarding composable not registered in NavGraph"; \
else \
echo "โ
Onboarding route and composable exist"; \
fi
echo "" && echo "--- Feature Gate Checks ---" && \
grep -rn "isOnboardingCompleted\|isSubscribed\|isPremium\|isVerified" app/src/main/java/**/nav/ || \
echo "โ ๏ธ No feature gates found in navigation - consider adding completion checks"
Required User Journey Patterns
Every app should verify these flows exist and work:
| Flow Type | Pattern | Checkpoint |
|---|
| First Launch | Splash โ Onboarding โ Dashboard | Onboarding must show for new users |
| New Registration | Register โ Onboarding โ Dashboard | Never skip onboarding for new accounts |
| Returning User (completed) | Login โ Dashboard | Only if onboarding was completed |
| Returning User (incomplete) | Login โ Onboarding โ Dashboard | Resume onboarding if not completed |
| Session Expired | Any Screen โ Login | Redirect to login on auth failure |
| Logout | Settings โ Login | Clear session and return to login |
| Deep Link | External โ Specific Screen | Handle auth state before showing content |
Flow Verification Checklist
Implementation Pattern for Feature Gates
composable(NavRoutes.Login.route) {
val onboardingRepository: OnboardingRepository = hiltViewModel<LoginViewModel>().onboardingRepository
LoginScreen(
onLoginSuccess = { userId ->
viewModelScope.launch {
val isCompleted = onboardingRepository.isOnboardingCompleted(userId)
.getOrDefault(false)
if (isCompleted) {
navController.navigate(NavRoutes.Dashboard.route) {
popUpTo(NavRoutes.Login.route) { inclusive = true }
}
} else {
navController.navigate(NavRoutes.Onboarding.route) {
popUpTo(NavRoutes.Login.route) { inclusive = true }
}
}
}
}
)
}
onLoginSuccess = {
navController.navigate(NavRoutes.Dashboard.route)
}
Quick Flow Detection Commands
grep -rn "navController.navigate\|navigate(" app/src/main/java/**/nav/ | \
grep -v "popBackStack" | head -20
echo "=== Potential Bypassed Feature Gates ===" && \
grep -B5 "NavRoutes.Dashboard" app/src/main/java/**/nav/*NavGraph.kt | \
grep -v "isOnboardingCompleted\|isCompleted\|checkOnboarding" && \
echo "โ ๏ธ Review above - Dashboard navigation may bypass required flows"
grep -rn "isOnboardingCompleted" app/src/main/java/ | wc -l | \
xargs -I {} sh -c 'if [ {} -eq 0 ]; then echo "โ No onboarding completion check found!"; fi'
๐ Mock Data Requirements for Repository Stubs
The Chart Data Problem
When implementing Repository stubs, NEVER return empty lists for data that powers UI charts or visualizations. This causes:
- Charts that render but show nothing (blank Canvas)
- Line charts that skip rendering (e.g.,
if (points.size < 2) return)
- Empty state screens even when data structure exists
Mock Data Rules
Rule 1: List data for charts MUST have at least 7 items
override suspend fun getWeeklySummary(...): Result<WeeklySummary> {
return Result.success(
WeeklySummary(
dailyReports = emptyList()
)
)
}
override suspend fun getWeeklySummary(...): Result<WeeklySummary> {
val mockDailyReports = (0 until 7).map { dayOffset ->
createMockDailyReport(
score = listOf(72, 78, 85, 80, 76, 88, 82)[dayOffset],
duration = listOf(390, 420, 450, 410, 380, 460, 435)[dayOffset]
)
}
return Result.success(
WeeklySummary(dailyReports = mockDailyReports)
)
}
Rule 2: Use realistic, varied sample values
scores = listOf(80, 80, 80, 80, 80, 80, 80)
scores = listOf(72, 78, 85, 80, 76, 88, 82)
Rule 3: Data must match domain model exactly
grep -A 20 "data class TherapyData" app/src/main/java
Rule 4: Create helper functions for complex mock data
private fun createMockEntity(param1: Int, param2: Int): YourDomainEntity {
return YourDomainEntity(
id = "mock_${System.currentTimeMillis()}",
field1 = param1,
field2 = NestedObject(value = param2, ...),
)
}
Quick Verification Commands for Mock Data
grep -rn "emptyList()\|listOf()" app/src/main/java/**/data/repository/*RepositoryImpl.kt
grep -rn "dailyReports\|weeklyData\|chartData" app/src/main/java/**/data/repository/ | grep -E "emptyList|= listOf\(\)"
๐ Cross-Repository ID Consistency
The ID Mismatch Problem
When multiple repositories reference the same entities, IDs MUST be consistent across all repositories. Mismatched IDs cause:
getById() returns null/failure even though data exists
- Navigation to detail screens fails silently
- Empty UI despite having mock data in the system
ID Consistency Rules
Rule 1: Use identical IDs across all Repository stubs
createMockEntity(id = "entity_1", name = "Item A")
Entity(id = "entity_001", ...)
createMockEntity(id = "entity_1", name = "Item A")
Entity(id = "entity_1", ...)
Rule 2: Verify IDs before implementing cross-repository features
grep -rn "id = \"[a-z]*_" app/src/main/java/**/data/repository/
Rule 3: Create ID constants for shared entities
object MockIds {
const val ENTITY_A = "entity_1"
const val ENTITY_B = "entity_2"
const val ENTITY_C = "entity_3"
}
Quick Verification Commands for ID Consistency
echo "=== Entity IDs across Repositories ===" && \
grep -oh "[a-z]*_[0-9a-zA-Z_]*" app/src/main/java/**/data/repository/*RepositoryImpl.kt | sort -u
grep -rn "id = \"[a-z]*_[0-9]" app/src/main/java/**/data/repository/ | head -20
๐ค Advanced Mock Data Prediction System
The Zero-Null Policy
CRITICAL RULE: Repository stub methods should NEVER return null or empty data.
When implementing Repository stubs, assume the app is being used by an active user with existing data. An empty app provides no value for UX testing.
Auto-Detection: Screen Type โ Required Data
Before implementing any screen, identify its type and predict required data:
| Screen Type | Detection Pattern | Required Mock Data |
|---|
| List Screen | LazyColumn, items(), forEach | List with 5-10 items, varied data |
| Detail Screen | getById(), Single item display | Complete entity with all fields |
| Chart/Report | Canvas, Chart, progress bars | 7+ data points with realistic variance |
| Form Screen | TextField, Button("Submit") | Pre-filled sample values |
| Dashboard | Multiple Card, summary stats | All metric cards populated |
| Empty State | if (list.isEmpty()) | NEVER trigger - always have data |
Prediction Matrix: Return Type โ Mock Strategy
override suspend fun getLatestReport(userId: String): Result<Report?> {
return Result.success(createMockReport())
}
override suspend fun getHistory(userId: String): Result<List<Record>> {
return Result.success((1..7).map { createMockRecord(it) })
}
override suspend fun getStats(): Result<Map<String, Int>> {
return Result.success(mapOf("score" to 85, "streak" to 7))
}
override fun observeStatus(): Flow<Status?> {
return flowOf(Status.Active)
}
Domain-Aware Mock Generation
Generate mock data that makes sense for your specific domain (defined by Spec):
score = 50
items = emptyList()
score = 85
items = listOf(item1, item2, item3)
Principle: Mock data should simulate a real user experience, not just compile successfully.
Pre-Flight Automated Checks
Run these before testing to catch empty UI issues:
grep -rn "return Result.success(null)" app/src/main/java/**/data/repository/
grep -rn "return Result.success(emptyList\|return Result.success(listOf()" app/src/main/java/**/data/repository/
grep -rn "flowOf(null)\|MutableStateFlow(null)" app/src/main/java/**/data/repository/
grep -rn "// TODO" -A1 app/src/main/java/**/data/repository/ | grep "return"
echo "=== Repository Methods Needing Mock Data ===" && \
grep -rh "suspend fun\|fun " app/src/main/java/**/domain/repository/*.kt | \
grep -E "Result<|Flow<" | \
grep -oE "[a-zA-Z]+\([^)]*\).*Result<[^>]+>|[a-zA-Z]+\([^)]*\).*Flow<[^>]+>"
Mock Data Completeness Checklist
Before marking a Repository stub as "done", verify:
๐จ Universal UI/UX Production Standards
Animation Requirements (Apply to ALL Apps)
navController.navigate(route) {
enterTransition = fadeIn() + slideInHorizontally()
exitTransition = fadeOut() + slideOutHorizontally()
}
LazyColumn {
itemsIndexed(items) { index, item ->
AnimatedVisibility(
enter = fadeIn() + slideInVertically(initialOffsetY = { it * (index + 1) })
) {
ItemCard(item)
}
}
}
SwipeRefresh(state = rememberSwipeRefreshState(isRefreshing)) {
LazyColumn { ... }
}
if (error != null) {
ErrorState(
message = error,
onRetry = { viewModel.retry() }
)
}
Accessibility Requirements
Icon(
imageVector = Icons.Default.Star,
contentDescription = "Rating: ${rating} stars"
)
Modifier
.size(48.dp)
.clickable { }
Row(Modifier.semantics(mergeDescendants = true) { }) {
Icon(...)
Text(...)
}
Edge Case Handling
when {
isLoading -> LoadingState()
error != null -> ErrorState()
data.isEmpty() -> EmptyState()
else -> ContentState(data)
}
@Composable
fun EmptyState() {
Column(horizontalAlignment = CenterHorizontally) {
Image(emptyStateIllustration)
Text("No data yet")
Text("Data will appear here after you start using the app")
Button(onClick = onAction) {
Text("Get Started")
}
}
}
๐ Production Completeness Verification
grep -rL "isLoading\|CircularProgressIndicator\|Shimmer" app/src/main/java/**/ui/screens/*.kt
grep -rL "error\|Error\|onRetry" app/src/main/java/**/ui/screens/*.kt
grep -rn "LazyColumn\|LazyRow" app/src/main/java/**/ui/screens/*.kt | while read line; do
file=$(echo $line | cut -d: -f1)
grep -L "isEmpty\|EmptyState\|empty" "$file"
done
grep -rn "clickable\|Button\|IconButton" app/src/main/java/**/ui/ | \
grep -v "contentDescription"
grep -rn "TODO\|FIXME\|placeholder\|Lorem\|Test" app/src/main/java/**/ui/
grep -rL "animat\|transition\|Animat" app/src/main/java/**/ui/screens/*.kt
Production Readiness Checklist
Before release, verify each screen has:
๐ Navigation Wiring Verification Guide
Problem: A Screen may define onNavigateToSettings: () -> Unit = {}, but if NavGraph doesn't pass a real callback, clicking does nothing.
Detection Pattern:
fun SettingsScreen(
onNavigateToAccountInfo: () -> Unit = {},
onNavigateToChangePassword: () -> Unit = {}
)
SettingsScreen(
onNavigateBack = { navController.popBackStack() }
)
SettingsScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToAccountInfo = { navController.navigate(NavRoutes.AccountInfo.route) },
onNavigateToChangePassword = { navController.navigate(NavRoutes.ChangePassword.route) }
)
Verification Script:
echo "=== Checking Navigation Wiring ===" && \
for screen in $(grep -rl "fun [A-Z][a-zA-Z]*Screen(" app/src/main/java/**/ui/screens/*.kt); do
SCREEN_NAME=$(basename "$screen" .kt)
echo "--- $SCREEN_NAME ---"
echo "Declared callbacks:"
grep -oE "onNavigateTo[A-Za-z]+" "$screen" | sort -u
echo "Wired in NavGraph:"
grep -A 20 "${SCREEN_NAME}(" app/src/main/java/**/nav/*NavGraph.kt 2>/dev/null | grep -oE "onNavigateTo[A-Za-z]+" | sort -u
echo ""
done
๐ ServiceโRepository Wiring Verification Guide
Problem: A Service may call repository.getAccountInfo(), but if the Repository interface doesn't have this method or RepositoryImpl doesn't implement it, the app crashes at runtime.
Detection Pattern:
class SettingsService(private val repository: SettingsRepository) {
suspend fun getAccountInfo() = repository.getAccountInfo()
}
interface SettingsRepository {
suspend fun getSettings(): Settings
}
class SettingsService(private val repository: SettingsRepository) {
suspend fun getAccountInfo() = repository.getAccountInfo()
}
interface SettingsRepository {
suspend fun getSettings(): Settings
suspend fun getAccountInfo(): AccountInfo
}
class SettingsRepositoryImpl : SettingsRepository {
override suspend fun getSettings() = ...
override suspend fun getAccountInfo() = ...
}
Verification Script:
echo "=== Checking ServiceโRepository Wiring ===" && \
for service in $(find app/src/main/java -name "*Service.kt" -path "*/domain/service/*"); do
SERVICE_NAME=$(basename "$service" .kt)
echo "--- $SERVICE_NAME ---"
echo "Repository methods called:"
grep -oE "repository\.[a-zA-Z]+\(" "$service" | sed 's/repository\.//' | sed 's/($//' | sort -u
done
0. Project Setup - CRITICAL
โ ๏ธ IMPORTANT: This reference project has been validated with tested Gradle settings and library versions. NEVER reconfigure project structure or modify build.gradle / libs.versions.toml, or it will cause compilation errors.
Step 1: Clone the reference project
git clone https://github.com/jrjohn/arcana-android.git [new-project-directory]
cd [new-project-directory]
Step 2: Reinitialize Git (remove original repo history)
rm -rf .git
git init
git add .
git commit -m "Initial commit from arcana-android template"
Step 3: Modify project name and package
Only modify the following required items:
rootProject.name in settings.gradle.kts
namespace and applicationId in app/build.gradle.kts
- Rename package directory structure under
app/src/main/java/
- Update package-related settings in
AndroidManifest.xml
โ ๏ธ Renaming the package touches every import statement, AndroidManifest.xml, and the Hilt DI modules โ do it via IDE refactor (Android Studio: Refactor > Rename on the package), NOT with sed/text replacement.
Step 4: Clean up example code
The cloned project contains example UI (e.g., Arcana User Management). Clean up and replace with new project screens:
Core architecture files to KEEP (do not delete):
core/ - Common utilities (analytics, common, cache)
di/ - Hilt DI modules
sync/ - Sync management
data/local/AppDatabase.kt - Room database base configuration
data/repository/ - Repository base classes
MainActivity.kt - Entry Activity
MyApplication.kt - Application class
nav/NavGraph.kt - Navigation configuration (modify routes)
Example files to REPLACE:
ui/screens/ - Delete all example screens, create new project UI
ui/theme/ - Modify Theme colors and styles
data/model/ - Delete example Models, create new Domain Models
data/local/dao/ - Delete example DAO, create new DAO
data/local/entity/ - Delete example Entity, create new Entity
data/network/ - Modify API endpoints
domain/ - Delete example Service, create new business logic
Step 5: Verify build
./gradlew clean build
โ Prohibited Actions
- DO NOT create new build.gradle.kts from scratch
- DO NOT modify version numbers in
gradle/libs.versions.toml
- DO NOT add or remove dependencies (unless explicitly required)
- DO NOT modify Gradle wrapper version
- DO NOT reconfigure Compose, Hilt, Room, or other library settings
โ
Allowed Modifications
- Add business-related Kotlin code (following existing architecture)
- Add UI screens (using existing Compose settings)
- Add Domain Models, Repository, ViewModel
- Modify strings.xml, colors.xml, and other resource files
- Add navigation routes
1. TDD & Spec-Driven Development Workflow - MANDATORY
โ ๏ธ CRITICAL: All development MUST follow this TDD workflow. Every Spec requirement must have corresponding tests BEFORE implementation.
๐จ ABSOLUTE RULE: TDD = Tests + Implementation. Writing tests without implementation is INCOMPLETE. Every test file MUST have corresponding production code that passes the tests.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TDD Development Workflow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Step 1: Analyze Spec โ Extract all SRS & SDD requirements โ
โ Step 2: Create Tests โ Write tests for EACH Spec item โ
โ Step 3: Verify Coverage โ Ensure 100% Spec coverage in tests โ
โ Step 4: Implement โ Build features to pass tests โ ๏ธ MANDATORY โ
โ Step 5: Mock APIs โ Use mock data for unfinished Cloud APIs โ
โ Step 6: Run All Tests โ ALL tests must pass before completion โ
โ Step 7: Verify 100% โ Tests written = Features implemented โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FORBIDDEN: Tests Without Implementation
Implementation Completion Checklist
Before marking ANY module complete, verify:
echo "=== TDD Completion Check ===" && \
TEST_COUNT=$(find app/src/test -name "*Test.kt" | wc -l) && \
IMPL_COUNT=$(find app/src/main -name "*.kt" | grep -E "(ViewModel|Repository|Service|Screen)" | wc -l) && \
echo "Test files: $TEST_COUNT" && \
echo "Implementation files: $IMPL_COUNT" && \
echo "If test count > impl count, you have INCOMPLETE TDD!"
Completion Criteria:
| Criteria | Required |
|---|
| Test file exists | โ
|
| Implementation file exists | โ
|
| All tests pass | โ
|
| No PlaceholderScreen in NavGraph | โ
|
| No "ๅณๅฐๆจๅบ" / "Coming Soon" text | โ
|
| No NotImplementedError | โ
|
| No empty onClick handlers | โ
|
โ PlaceholderScreen Policy
PlaceholderScreen is ONLY allowed as a temporary navigation target during active development. It is FORBIDDEN as a final state.
composable(NavRoutes.Feature.route) {
PlaceholderScreen(title = "Feature")
}
composable(NavRoutes.Feature.route) {
FeatureScreen(
viewModel = hiltViewModel(),
onNavigateBack = { navController.popBackStack() }
)
}
PlaceholderScreen Cleanup Check:
grep -rn "PlaceholderScreen\|ๅณๅฐๆจๅบ\|Coming Soon" app/src/main/java/
Step 1: Analyze Spec Documents
Before writing any code, extract ALL requirements from specification documents:
Step 2: Create Test Cases for Each Spec Item
@HiltAndroidTest
class LoginViewModelTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
private lateinit var viewModel: LoginViewModel
private lateinit var mockAuthRepository: AuthRepository
@Before
fun setup() {
mockAuthRepository = mockk()
viewModel = LoginViewModel(mockAuthRepository)
}
@Test
fun `login with valid credentials should succeed`() = runTest {
coEvery { mockAuthRepository.login("test@test.com", "password123") } returns Result.success(Unit)
viewModel.onInput(LoginViewModel.Input.UpdateEmail("test@test.com"))
viewModel.onInput(LoginViewModel.Input.UpdatePassword("password123"))
viewModel.onInput(LoginViewModel.Input.Submit)
assertTrue(viewModel.output.value.isLoginSuccess)
}
@Test
fun `login with invalid credentials should show error`() = runTest {
coEvery { mockAuthRepository.login(any(), any()) } returns Result.failure(Exception("Invalid credentials"))
viewModel.onInput(LoginViewModel.Input.Submit)
assertNotNull(viewModel.output.value.loginError)
}
}
Step 3: Spec Coverage Verification Checklist
Before implementation, verify ALL requirements have tests:
Step 4: Mock API Implementation - MANDATORY
โ ๏ธ CRITICAL: Every Repository method MUST return valid mock data. NEVER leave methods returning NotImplementedError or TODO.
Rules for Mock Repositories:
- ALL repository methods must return
Result.success(...) with realistic mock data
- Use
delay() to simulate network latency (500-1000ms)
- Mock data must match the domain model structure exactly
- Check enum values exist before using them (e.g.,
TrendDirection.IMPROVING not TrendDirection.UP)
- Include all required constructor parameters for data classes
For APIs not yet available from Cloud team, implement mock repositories:
class MockAuthRepository @Inject constructor(
private val sharedPreferences: SharedPreferences
) : AuthRepository {
companion object {
private val MOCK_USERS = listOf(
MockUser("test@test.com", "password123", "Test User"),
MockUser("demo@demo.com", "demo123", "Demo User")
)
}
override suspend fun login(email: String, password: String): Result<Unit> {
delay(1000)
val user = MOCK_USERS.find { it.email == email && it.password == password }
return if (user != null) {
sharedPreferences.edit()
.putString("access_token", "mock_token_${System.currentTimeMillis()}")
.putString("user_name", user.name)
.apply()
Result.success(Unit)
} else {
Result.failure(Exception("Invalid email or password"))
}
}
override suspend fun isLoggedIn(): Boolean {
return sharedPreferences.getString("access_token", null) != null
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindAuthRepository(
impl: MockAuthRepository
): AuthRepository
}
Step 5: UI Completeness Check - MANDATORY
โ ๏ธ CRITICAL: Before completing any screen, verify ALL interactive elements work.
UI Checklist for Each Screen:
Step 5.1: Navigation Graph Verification - MANDATORY
โ ๏ธ CRITICAL: Every route in NavRoutes MUST have a corresponding composable() destination in the NavGraph. Missing routes cause runtime crashes.
Quick Check Commands:
grep -E "data object|NavRoutes\(" app/src/main/java/**/nav/NavRoutes.kt
grep -E "composable\(NavRoutes\." app/src/main/java/**/nav/*NavGraph.kt
grep -rn "navController.navigate\(NavRoutes\." app/src/main/java/**/nav/
Verification Checklist:
Common Navigation Errors:
data object ScreenA : NavRoutes("screen_a")
data object ScreenA : NavRoutes("screen_a")
composable(NavRoutes.ScreenA.route) {
ScreenAScreen(
onNavigateBack = { navController.popBackStack() }
)
}
composable(NavRoutes.ScreenA.route) {
PlaceholderScreen(
title = "Screen A",
onNavigateBack = { navController.popBackStack() }
)
}
Placeholder Screen Template:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PlaceholderScreen(
title: String,
onNavigateBack: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(title) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
}
)
}
) { padding ->
Box(
modifier = Modifier.fillMaxSize().padding(padding),
contentAlignment = Alignment.Center
) {
Text("Coming Soon", style = MaterialTheme.typography.bodyLarge)
}
}
}
Common Empty Handler Patterns to Avoid:
onClick = { }
onClick = { }
onClick = { }
onClick = onNavigateToSettings
onClick = { viewModel.performAction() }
onClick = { navController.navigate(NavRoutes.Settings.route) }
Step 6: Run All Tests Before Completion
./gradlew test
./gradlew connectedAndroidTest
./gradlew jacocoTestReport
./gradlew check
Step 7: Final Verification - MANDATORY
โ ๏ธ CRITICAL: Before marking any feature complete, perform these checks:
./gradlew :app:compileDebugKotlin
grep -r "NotImplementedError\|TODO.*implement" app/src/main/java/**/repository/
grep -r "onClick\s*=\s*{\s*}" app/src/main/java/**/ui/
grep -r "hiltViewModel()" app/src/main/java/**/ui/screens/
echo "=== Navigation Verification ===" && \
ROUTES=$(grep -c "data object" app/src/main/java/**/nav/NavRoutes.kt 2>/dev/null || echo 0) && \
COMPOSABLES=$(grep -c "composable(NavRoutes\." app/src/main/java/**/nav/*NavGraph.kt 2>/dev/null || echo 0) && \
echo "Routes defined: $ROUTES" && \
echo "Composables registered: $COMPOSABLES" && \
if [ "$ROUTES" -ne "$COMPOSABLES" ]; then echo "โ MISMATCH! Missing $(($ROUTES - $COMPOSABLES)) composable destinations"; else echo "โ
All routes have destinations"; fi
Final Checklist:
๐จ 100% Implementation Verification - MANDATORY
Before completing ANY task, run this comprehensive check:
echo "=== 1. PlaceholderScreen Check (MUST be empty) ===" && \
grep -rn "PlaceholderScreen\|ๅณๅฐๆจๅบ\|Coming Soon" app/src/main/java/ || echo "โ
No placeholders found"
echo "" && echo "=== 2. NotImplementedError Check (MUST be empty) ===" && \
grep -rn "NotImplementedError\|TODO.*implement\|throw.*NotImplemented" app/src/main/java/ || echo "โ
No NotImplementedError found"
echo "" && echo "=== 3. Empty Handler Check (MUST be empty) ===" && \
grep -rn "onClick\s*=\s*{\s*}\|onClick\s*=\s*{.*TODO" app/src/main/java/ || echo "โ
No empty handlers found"
echo "" && echo "=== 4. Test vs Implementation Parity ===" && \
echo "Test ViewModels:" && find app/src/test -name "*ViewModelTest.kt" 2>/dev/null | wc -l && \
echo "Impl ViewModels:" && find app/src/main -name "*ViewModel.kt" 2>/dev/null | wc -l && \
echo "(Counts should be equal or impl > test)"
echo "" && echo "=== 5. Build Verification ===" && \
./gradlew :app:compileDebugKotlin --quiet && echo "โ
Build successful" || echo "โ Build failed"
echo "" && echo "=== 6. Run All Tests ===" && \
./gradlew test --quiet && echo "โ
All tests passed" || echo "โ Tests failed"
echo "" && echo "=== 7. ๐จ Navigation Wiring Check (CRITICAL!) ===" && \
echo "Screen navigation callbacks:" && \
SCREEN_CALLBACKS=$(grep -roh "onNavigateTo[A-Za-z]*:" app/src/main/java/**/ui/screens/*.kt 2>/dev/null | grep -oE "onNavigateTo[A-Za-z]+" | sort -u | wc -l) && \
echo " Declared: $SCREEN_CALLBACKS" && \
WIRED_CALLBACKS=$(grep -roh "onNavigateTo[A-Za-z]*\s*=" app/src/main/java/**/nav/*NavGraph.kt 2>/dev/null | grep -oE "onNavigateTo[A-Za-z]+" | sort -u | wc -l) && \
echo " Wired: $WIRED_CALLBACKS" && \
if [ "$SCREEN_CALLBACKS" -ne "$WIRED_CALLBACKS" ]; then \
echo "โ MISMATCH! $(($SCREEN_CALLBACKS - $WIRED_CALLBACKS)) callbacks not wired in NavGraph"; \
echo "Unwired callbacks:"; \
comm -23 <(grep -roh "onNavigateTo[A-Za-z]*:" app/src/main/java/**/ui/screens/*.kt 2>/dev/null | grep -oE "onNavigateTo[A-Za-z]+" | sort -u) \
<(grep -roh "onNavigateTo[A-Za-z]*\s*=" app/src/main/java/**/nav/*NavGraph.kt 2>/dev/null | grep -oE "onNavigateTo[A-Za-z]+" | sort -u); \
else \
echo "โ
All navigation callbacks properly wired"; \
fi
โ Task is NOT complete if ANY of these checks fail:
- PlaceholderScreen found in code
- "ๅณๅฐๆจๅบ" or "Coming Soon" text found
- NotImplementedError found
- Empty onClick handlers found
- Test count > Implementation count
- Build fails
- Tests fail
- Navigation callbacks declared > callbacks wired in NavGraph (clicking does nothing!)
Test Directory Structure
app/src/
โโโ main/java/... # Production code
โโโ test/java/... # Unit tests
โ โโโ ui/screens/
โ โ โโโ auth/
โ โ โ โโโ LoginViewModelTest.kt
โ โ โ โโโ RegisterViewModelTest.kt
โ โ โโโ dashboard/
โ โ โ โโโ DashboardViewModelTest.kt
โ โ โโโ splash/
โ โ โโโ SplashViewModelTest.kt
โ โโโ domain/
โ โ โโโ service/
โ โ โโโ UserServiceTest.kt
โ โโโ data/
โ โโโ repository/
โ โโโ AuthRepositoryTest.kt
โโโ androidTest/java/... # Instrumented tests
โโโ ui/screens/
โโโ LoginScreenTest.kt
โโโ DashboardScreenTest.kt
2. Project Structure
app/
โโโ presentation/ # UI Layer
โ โโโ ui/ # Compose Composables
โ โโโ viewmodel/ # Input/Output ViewModel
โ โโโ navigation/ # Navigation Logic
โโโ domain/ # Domain Layer
โ โโโ model/ # Domain Models
โ โโโ service/ # Business Services
โ โโโ repository/ # Repository Interfaces
โโโ data/ # Data Layer
โโโ repository/ # Repository Implementations
โโโ local/ # Room Database
โ โโโ entity/ # Database Entities
โ โโโ dao/ # Data Access Objects
โโโ remote/ # API Client
โโโ api/ # API Interfaces
โโโ dto/ # Data Transfer Objects
2. ViewModel Input/Output Pattern
class UserViewModel @Inject constructor(
private val userService: UserService
) : ViewModel() {
sealed interface Input {
data class UpdateName(val name: String) : Input
data class UpdateEmail(val email: String) : Input
data object Submit : Input
}
data class Output(
val name: String = "",
val email: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
private val _output = MutableStateFlow(Output())
val output: StateFlow<Output> = _output.asStateFlow()
private val _effect = Channel<Effect>()
val effect = _effect.receiveAsFlow()
sealed interface Effect {
data object NavigateBack : Effect
data class ShowSnackbar(val message: String) : Effect
}
fun onInput(input: Input) {
when (input) {
is Input.UpdateName -> updateName(input.name)
is Input.UpdateEmail -> updateEmail(input.email)
is Input.Submit -> submit()
}
}
}
3. Offline-First Strategy
class UserRepository @Inject constructor(
private val userDao: UserDao,
private val userApi: UserApi,
private val syncManager: SyncManager
) : IUserRepository {
override fun getUsers(): Flow<List<User>> = userDao.getAllUsers()
.map { entities -> entities.map { it.toDomain() } }
override suspend fun updateUser(user: User): Result<Unit> {
userDao.update(user.toEntity().copy(syncStatus = SyncStatus.PENDING))
syncManager.scheduleSyncWork()
return Result.success(Unit)
}
suspend fun syncPendingChanges() {
val pendingUsers = userDao.getPendingSync()
pendingUsers.forEach { entity ->
try {
userApi.updateUser(entity.toDto())
userDao.update(entity.copy(syncStatus = SyncStatus.SYNCED))
} catch (e: Exception) {
}
}
}
}
4. Three-Layer Cache Strategy
class CacheManager<K, V>(
private val maxMemorySize: Int = 100,
private val ttlMillis: Long = 5 * 60 * 1000
) {
private val memoryCache = LruCache<K, CacheEntry<V>>(maxMemorySize)
private val lruCache = LinkedHashMap<K, CacheEntry<V>>(16, 0.75f, true)
data class CacheEntry<V>(
val value: V,
val timestamp: Long = System.currentTimeMillis()
) {
fun isExpired(ttl: Long) = System.currentTimeMillis() - timestamp > ttl
}
suspend fun get(key: K, loader: suspend () -> V): V {
memoryCache.get(key)?.takeIf { !it.isExpired(ttlMillis) }?.let {
return it.value
}
lruCache[key]?.takeIf { !it.isExpired(ttlMillis) }?.let {
memoryCache.put(key, it)
return it.value
}
val value = loader()
val entry = CacheEntry(value)
memoryCache.put(key, entry)
lruCache[key] = entry
return value
}
}
5. Compose UI Best Practices
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel()
) {
val output by viewModel.output.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
is UserViewModel.Effect.NavigateBack -> navController.popBackStack()
is UserViewModel.Effect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message)
}
}
}
UserContent(
output = output,
onInput = viewModel::onInput
)
}
@Composable
private fun UserContent(
output: UserViewModel.Output,
onInput: (UserViewModel.Input) -> Unit
) {
Column {
OutlinedTextField(
value = output.name,
onValueChange = { onInput(UserViewModel.Input.UpdateName(it)) },
label = { Text("Name") }
)
}
}
6. Hilt Dependency Injection
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.fallbackToDestructiveMigration()
.build()
@Provides
fun provideUserDao(database: AppDatabase): UserDao = database.userDao()
@Provides
@Singleton
fun provideHttpClient(): HttpClient = HttpClient(OkHttp) {
install(ContentNegotiation) { json() }
install(Logging) { level = LogLevel.BODY }
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepository): IUserRepository
}
7. Form Validation
class FormState {
var email by mutableStateOf("")
var emailTouched by mutableStateOf(false)
val emailError by derivedStateOf {
when {
!emailTouched -> null
email.isBlank() -> "Email is required"
!email.isValidEmail() -> "Invalid email format"
else -> null
}
}
val isValid by derivedStateOf {
email.isNotBlank() && email.isValidEmail()
}
}
fun String.isValidEmail(): Boolean =
android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
Code Review Checklist
Required Items
Performance Checks
Security Checks
Common Issues
Gradle Build Issues
- Check
gradle/libs.versions.toml for version conflicts
- Run
./gradlew --refresh-dependencies
- Clear cache with
./gradlew clean
Compose Preview Failures
- Ensure
@Preview functions have no required parameters or have default values
- Check Hilt ViewModel uses
@HiltViewModel
- Use mock data in Preview instead of real ViewModel
Room Migration Issues
- Define Migration objects
- Consider
fallbackToDestructiveMigration() during development
- Test migration paths
Tech Stack Reference
| Technology | Recommended Version |
|---|
| Kotlin | 2.x (K2) |
| Compose BOM | see gradle/libs.versions.toml in the reference repo for current versions |
| Hilt | 2.50+ |
| Room | 2.6+ |
| Ktor | 2.3+ |
| Coroutines | 1.8+ |
๐ฎ Spec Gap Prediction System (Universal)
Overview
When Spec is incomplete, use these universal UI/UX rules to predict missing elements. This is NOT about adding domain-specific features, but ensuring logical completeness of what's already defined.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Spec Gap Prediction System โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Input: Existing Spec (screens, features, data models) โ
โ โ
โ Output: Predicted gaps based on universal UI/UX patterns โ
โ โ
โ Principle: If Spec defines A, universally A requires B โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Screen Type โ Required States (Universal)
When Spec defines a screen type, these states are universally required:
| Screen Type | Required States | Prediction Rule |
|---|
| List Screen | Loading, Empty, Error, Data, Pull-to-refresh | Lists must have empty state |
| Detail Screen | Loading, Error, Data, Not Found | Details must have loading state |
| Form Screen | Input, Validation, Submitting, Success, Error | Forms must have validation |
| Dashboard | Loading skeleton, Partial data, Full data | Dashboards must have skeleton |
| Settings | Current values, Save confirmation | Settings must have confirmation |
Flow Completion Prediction
When Spec defines a feature, predict related flows:
| If Spec Has | Predict Also Needed | Reasoning |
|---|
| Login | Register, Forgot Password | Login requires register |
| Register | Onboarding, Email Verification | Register requires onboarding |
| List | Detail, Search, Filter | List requires detail |
| Detail | Edit (if editable), Share, Delete | Detail often has edit |
| Create | Edit, Delete, Duplicate | Create requires modify |
| Profile | Edit Profile, Logout | Profile requires logout |
| Notification List | Notification Detail, Mark Read | Notifications require read status |
| Cart | Checkout, Remove Item | Cart requires checkout |
echo "=== Flow Completion Prediction ===" && \
grep -l "Login" app/src/main/java/**/nav/NavRoutes.kt && \
(grep -l "Register\|SignUp" app/src/main/java/**/nav/NavRoutes.kt || \
echo "โ ๏ธ Login exists but Register not found - predict needed")
grep -l "Register" app/src/main/java/**/nav/NavRoutes.kt && \
(grep -l "Onboarding" app/src/main/java/**/nav/NavRoutes.kt || \
echo "โ ๏ธ Register exists but Onboarding not found - predict needed")
for list in $(grep -oh "[A-Z][a-z]*List" app/src/main/java/**/nav/NavRoutes.kt); do
detail="${list%List}Detail"
grep -q "$detail" app/src/main/java/**/nav/NavRoutes.kt || \
echo "โ ๏ธ $list exists but $detail not found - predict needed"
done
Data Operation Prediction (CRUD)
When Spec defines data operations, predict the complete CRUD cycle:
| If Spec Has | Predict Also Needed |
|---|
| Create only | Read, Update, Delete |
| Read only | (May be intentional - verify with Spec) |
| List + Create | Detail, Edit, Delete |
| Detail only | List (how to navigate here?) |
Navigation Completeness Prediction
| Pattern | Prediction |
|---|
| Forward navigation exists | Back navigation required |
| Deep link target | Auth check required |
| Tab navigation | Content for ALL tabs |
| Bottom nav item | Screen for each item |
| Drawer menu item | Screen for each item |
echo "=== Navigation Completeness ===" && \
ROUTES=$(grep -oh "data object [A-Za-z]* :" app/src/main/java/**/nav/NavRoutes.kt | wc -l) && \
DESTINATIONS=$(grep -c "composable(NavRoutes\." app/src/main/java/**/nav/*NavGraph.kt) && \
echo "Routes defined: $ROUTES" && \
echo "Destinations implemented: $DESTINATIONS" && \
if [ "$ROUTES" -ne "$DESTINATIONS" ]; then \
echo "โ ๏ธ Mismatch! Some routes may be missing destinations"; \
fi
grep -A 20 "BottomNavigation\|NavigationBar" app/src/main/java/**/ui/ | \
grep -oh "NavRoutes\.[A-Za-z]*" | sort -u
UI State Prediction Matrix
For any screen, predict required UI states:
Spec Gap Detection Commands
echo "=== 1. Missing Screen States ===" && \
grep -L "Loading\|isLoading\|CircularProgress" app/src/main/java/**/ui/screens/*.kt 2>/dev/null | \
head -5 && echo "(screens may be missing loading state)"
echo "" && echo "=== 2. Missing Empty States ===" && \
grep -l "LazyColumn\|LazyRow" app/src/main/java/**/ui/screens/*.kt | \
xargs grep -L "empty\|Empty\|ๅฐ็ก\|ๆซ็ก" 2>/dev/null | \
head -5 && echo "(lists may be missing empty state)"
echo "" && echo "=== 3. Missing Error States ===" && \
grep -L "Error\|error\|้ฏ่ชค\|ๅคฑๆ" app/src/main/java/**/ui/screens/*.kt 2>/dev/null | \
head -5 && echo "(screens may be missing error state)"
echo "" && echo "=== 4. Incomplete Navigation Flows ===" && \
grep "data object" app/src/main/java/**/nav/NavRoutes.kt | \
grep -oh "[A-Z][a-zA-Z]*" | while read route; do \
grep -q "NavRoutes.$route" app/src/main/java/**/nav/*NavGraph.kt || \
echo "โ ๏ธ $route has no composable destination"; \
done
Prediction Summary Template
When analyzing Spec, generate this checklist:
## Spec Gap Analysis for [Feature Name]
### Defined in Spec:
- [ ] List: [ScreenName]List
- [ ] Detail: [ScreenName]Detail
- [ ] Create: Create[ScreenName]
### Predicted Gaps (based on universal patterns):
#### Screen States:
- [ ] Loading state for all screens
- [ ] Empty state for list screens
- [ ] Error state with retry for all screens
- [ ] Pull-to-refresh for list screens
#### Flow Completeness:
- [ ] [ScreenName]List โ [ScreenName]Detail navigation
- [ ] Create โ success โ navigate to list/detail
- [ ] Edit capability if data is user-generated
- [ ] Delete with confirmation dialog
#### Navigation:
- [ ] Back navigation from all screens
- [ ] Deep link support for detail screens
### Recommended Actions:
1. Confirm with stakeholders if Edit/Delete are needed
2. Implement Loading/Empty/Error states
3. Add navigation wiring for predicted flows
๐ง UX Completeness Verification (Universal)
Overview
This section provides universal verification commands that apply to ALL app types. Domain-specific features should be defined in the Spec, not in this SKILL.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UX Completeness Verification System โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Level 1: Code Verification โ
โ - Compile errors, empty handlers, navigation wiring โ
โ โ
โ Level 2: Visual Completeness โ
โ - Empty states, blank content areas, placeholder text โ
โ โ
โ Level 3: User Flow Completeness โ
โ - Ensure all user journeys have logical endpoints โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Quick UX Verification Commands
grep -rn "ๅฐ็ก\|ๆซ็ก\|ๅณๅฐ\|Coming Soon\|No data\|Empty\|TODO.*UI" app/src/main/java/**/ui/
grep -rn "emptyList()\|listOf()" app/src/main/java/**/ui/screens/**/.*ViewModel.kt
grep -rn "Icons.Default\|Icons.Filled" app/src/main/java/**/ui/screens/ | grep -v "ArrowBack\|Close\|Menu"
grep -rn -A 10 "TabRow\|Tab(" app/src/main/java/**/ui/screens/
๐ Screen-by-Screen UX Checklist
For EVERY screen, verify these items: