This file provides guidance to Claude Code (claude.ai/code) and other AI assistants when working with code in this repository. Detailed rules, coding standards, and project instructions are maintained in .agents/rules.md.
HustleHub — native Android campus peer-to-peer service marketplace for Meru University of Science & Technology (MUST). Only @must.ac.ke emails are permitted.
# Build and install debug APK on connected device/emulator
./gradlew installDebug
# Assemble without installing
./gradlew assembleDebug
# Unit tests
./gradlew test
# Single test class
./gradlew test --tests "must.kdroiders.hustlehub.ui.auth.SignUpViewModelTest"
# Instrumented tests (requires running emulator + backend on localhost:8080)
./gradlew connectedAndroidTest
# Lint — ktlint (style) + detekt (static analysis)
./gradlew ktlintCheck detekt
# Auto-fix ktlint formatting
./gradlew ktlintFormatRequired before first build:
cp keys.properties.template keys.properties
# Edit keys.properties with real values:
# MAPS_API_KEY, GEMINI_API_KEY, GOOGLE_WEB_CLIENT_ID
# Place google-services.json in app/The Spring Boot backend must be running locally (./gradlew bootRun in the backend repo) for the app to function. The emulator reaches it at http://10.0.2.2:8080/api/v1/.
Clean Architecture + MVVM, feature-sliced:
Composable Screen → ViewModel → UseCase → Repository interface
↓
RepositoryImpl
├── Retrofit (remote)
└── Room DAO (cache)
- Screens are
@Composablefunctions — zero business logic. - ViewModels expose
StateFlow<UiState<T>>; one-time events viaChannel<UiEvent>. - Use cases are single-
invokeclasses returningFlow<Result<T>>orsuspend fun: Result<T>. - Repositories coordinate remote + local; Room is the single source of truth.
- No
LiveDataanywhere —StateFlow/Flowonly.
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String) : UiState<Nothing>
}
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val cause: Throwable? = null) : Result<Nothing>()
}Every screen must handle all three UiState branches.
must.kdroiders.hustlehub/
├── activities/ # MainActivity — hosts HustleHubNav
├── appHilt/ # HustleHubApp (@HiltAndroidApp)
├── data/ # Shared models + repository interfaces/impls
│ ├── model/ # User, Service data classes
│ └── repository/ # AuthRepository, UserRepository, StorageRepository
├── datastore/ # UserPreferences (DataStore)
├── di/ # Hilt modules (AppModule, SupabaseModule)
├── navigation/ # Navigation 3 — NavKeys + NavGraph + MainScaffold
├── onboarding/ # OnboardingScreen + OnboardingViewModel
├── sharedComposables/ # Design-system components (HustleButton, HustleCard, …)
├── splash/ # SplashScreen + SplashViewModel (auth gate)
├── ui/
│ ├── auth/ # Login, SignUp, EmailVerification screens + ViewModels
│ └── features/ # chat, home, map, profile, profilesetup
└── util/ # ImageUtils
The target architecture in
[.agents/rules.md](.agents/rules.md)places all features underfeature/<name>/with fulldata/domain/presentationsub-splits. Currentlyauth,profile, andchathave full domain/data/presentation splits; other features will migrate to this structure as they are built out.
Uses Navigation 3 (androidx.navigation3) exclusively — no legacy NavController.
- All routes are
@SerializableNavKeyobjects defined innavigation/HustleNavKeys.kt— add every new destination there. - Root backstack starts at
Splash;SplashViewModelgates auth and routes toOnboarding,Login,ProfileSetup, orMainShell. MainShellhosts a nestedNavDisplayfor the four bottom-tab destinations (BottomHome,BottomMap,BottomChat,BottomProfile).- Never pass complex objects as NavKey arguments — pass IDs only, load data in the destination ViewModel.
- Every ViewModel:
@HiltViewModel+@Inject constructor(…). - Firebase dependencies (
FirebaseAuth,FirebaseFirestore,FirebaseStorage) are provided as nullable; the app hasNoop*fallbacks so it runs without Firebase (useful in CI / tests). - Inject at ViewModel level only — never directly into Composables.
- IMPORTANT: When getting a ViewModel in a Composable, DO NOT use
import androidx.hilt.navigation.compose.hiltViewModel. Instead, useimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel.
- Firebase Auth (email/password + Google Sign-In) for identity only.
- After sign-in, Firebase ID token is attached to every API request via
AuthInterceptor(Authorization: Bearer <token>). - All application data and media files go through the Spring Boot backend REST API (
/api/v1/) — Firebase Firestore, Realtime Database, and Supabase are not used for app data. - Media upload is handled via multipart POST requests to the Spring Boot backend (
/api/v1/media/upload), which handles storage via Firebase Storage bucket.
- Material 3 Expressive (
material3 = "1.5.0-alpha15") — use@OptIn(ExperimentalMaterial3ExpressiveApi::class)where needed. - All colors via
MaterialTheme.colorScheme, typography viaMaterialTheme.typography, shapes viaMaterialTheme.shapes— no hardcoded hex values in Composables. - Shared reusable composables live in
sharedComposables/. - Support both light and dark mode.
- Composables should stay under ~150 lines; split if larger.
- ktlint enforces style; detekt (
detekt.yml) enforces structure (LongMethod ≤ 60 lines, LargeClass ≤ 600 lines). Both are configured withignoreFailures = trueso they report but don't fail the build during development. - Use
Timberfor all logging — neverLog.*directly. Log atDEBUGonly for network calls; never log in release. - All user-facing strings must be in
strings.xml. - Named constants only — no magic strings or numbers. Use
companion objector top-levelobject.
- Branches:
main→dev→feature/<name> - All PRs target
dev, nevermaindirectly. - Conventional Commits:
feat(auth):,fix(chat):,refactor(core):,test(discovery):,docs:,chore: - PR naming:
[FEATURE] Add AI search screen/[FIX] Chat scroll position bug - Run
./gradlew ktlintCheck detekt testbefore opening a PR.