Architecture Overview
Archery Apprentice follows a Clean Architecture approach with MVVM (Model-View-ViewModel) pattern and Repository Pattern for data access.
Layered Architecture
┌─────────────────────────────────────────┐
│ UI Layer (Compose) │
│ - Screens & Composables │
│ - Navigation │
│ - User Input Handling │
└────────────┬────────────────────────────┘
│ observes StateFlow
↓
┌─────────────────────────────────────────┐
│ ViewModel Layer (State) │
│ - State Management (StateFlow) │
│ - Business Logic Coordination │
│ - UI State Transformation │
└────────────┬────────────────────────────┘
│ calls methods
↓
┌─────────────────────────────────────────┐
│ Repository Layer (Business Logic) │
│ - Data Operations │
│ - Business Rules │
│ - Multi-Source Coordination │
└────────────┬────────────────────────────┘
│ queries / writes
↓
┌─────────────────────────────────────────┐
│ Database Layer (Room DAOs) │
│ - SQL Queries │
│ - Transactions │
│ - Data Persistence │
└─────────────────────────────────────────┘
UI Layer (Composables)
Purpose: Present data and handle user interactions using Jetpack Compose.
Key Screens
Equipment Screens
BowSetupDetailScreen.kt- Equipment setup managementEquipmentListScreen.kt- Equipment inventory- Component-specific screens for each equipment type
Scoring Screens
ActiveScoringScreen.kt(1,958 lines) - Live scoring interfaceRoundDetailsScreen.kt- Round history and statisticsRoundCreationScreen.kt- Create new rounds
Tournament Screens
TournamentDiscoveryScreen.kt- Browse tournamentsTournamentDetailsScreen.kt- Tournament info and leaderboardLiveLeaderboardCard.kt- Real-time rankings
Navigation
AppNavigation.kt- Main navigation graphTournamentNavGraph.kt- Tournament sub-navigation
Pattern
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsState()
// UI reacts to state changes
when (state) {
is UiState.Loading -> LoadingIndicator()
is UiState.Success -> SuccessContent(state.data)
is UiState.Error -> ErrorMessage(state.message)
}
}Learn More: UI-Layer-Patterns
ViewModel Layer (State Management)
Purpose: Manage UI state, coordinate business logic, and expose data streams to UI.
ViewModels by Feature
1. Equipment ViewModels (13 total)
→ See Equipment-ViewModels for details
Component ViewModels:
ArrowViewModel→ usesArrowRepositoryStabilizerViewModel→ usesStabilizerRepositorySightViewModel→ usesSightRepositoryRiserViewModel→ usesRiserRepositoryLimbsViewModel→ usesLimbsRepositoryBowStringViewModel→ usesBowStringRepositoryPlungerViewModel→ usesPlungerRepositoryRestViewModel→ usesRestRepositoryAccessoryViewModel→ usesAccessoryRepositoryWeightViewModel→ usesWeightRepository
Setup ViewModels:
BowSetupViewModel→ usesBowSetupRepositoryEquipmentListViewModel→ coordinates equipment display
2. Scoring ViewModels (8 total)
→ See Scoring-ViewModels for details
Distributed Architecture (Extracted from 3,000-line monolith):
RoundViewModel(2,079 lines) - Core orchestrationLiveScoringViewModel(2,304 lines) - Active scoring (being refactored)RoundCreationViewModel(480 lines) - Round setup ✅RoundAnalyticsViewModel(605 lines) - Statistics ✅RoundManagementViewModel(495 lines) - Round lifecycle ✅RoundDisplayViewModel(216 lines) - UI formatting ✅RoundNavigationViewModel(192 lines) - Navigation ✅RoundScoringViewModel(187 lines) - Scoring logic ✅
3. Tournament ViewModels (3 total)
→ See Tournament-ViewModels for details
TournamentDiscoveryViewModel→ usesTournamentRepositoryTournamentDetailsViewModel→ usesTournamentRepositoryTournamentCreationViewModel→ usesTournamentRepository
4. Other ViewModels
AuthenticationViewModel→ usesAuthenticationRepositorySettingsViewModel→ usesSettingsRepository
Common ViewModel Pattern
@HiltViewModel
class MyViewModel @Inject constructor(
private val repository: MyRepository
) : ViewModel() {
// State exposed to UI
private val _uiState = MutableStateFlow(MyUiState())
val uiState: StateFlow<MyUiState> = _uiState.asStateFlow()
// Input state for forms
private val _inputState = MutableStateFlow(InputState())
val inputState: StateFlow<InputState> = _inputState.asStateFlow()
// Methods called by UI
fun updateField(value: String) {
_inputState.value = _inputState.value.copy(field = value)
}
suspend fun save(): SaveResult {
// Validate, call repository, handle result
}
}Learn More: ViewModel-Patterns
Repository Layer (Business Logic)
Purpose: Abstract data sources, implement business rules, coordinate between multiple data sources.
Repositories by Feature
Equipment Repositories (10 total)
→ See Equipment-Repositories for details
ArrowRepository- Arrow CRUD operationsStabilizerRepository- Stabilizer CRUD operationsSightRepository- Sight CRUD operationsRiserRepository- Riser CRUD operationsLimbsRepository- Limbs CRUD operationsBowStringRepository- Bow string CRUD operationsPlungerRepository- Plunger CRUD operationsRestRepository- Rest CRUD operationsAccessoryRepository- Accessory CRUD operationsWeightRepository- Weight CRUD operationsBowSetupRepository- Bow setup coordinationEquipmentStatsRepository- Equipment statistics
Scoring Repositories
→ See Scoring-Repositories for details
RoundRepository(1,443 lines) - Round and score operations- Issue: God class - should be split into 3 repositories
- See Checkpoint-Findings for refactoring plan
Tournament Repositories (3 implementations)
→ See Tournament-Repositories for details
Interface: TournamentRepository (395 lines, 51 methods)
- Issue: Interface too large (should have ~15 methods)
Implementations:
FirebaseTournamentRepository(1,707 lines) - Firestore-basedOfflineTournamentRepository(908 lines) - Local-onlyHybridTournamentRepository(1,506 lines) - Offline-first with sync
Factory: RepositoryFactory - Creates appropriate repository based on mode
Common Repository Pattern
class MyRepository @Inject constructor(
private val dao: MyDao
) {
// Return Flow for observable data
fun getAll(): Flow<List<MyEntity>> = dao.getAll()
// Suspend functions for one-time operations
suspend fun insert(entity: MyEntity)
suspend fun update(entity: MyEntity)
suspend fun delete(entity: MyEntity)
suspend fun getById(id: Long): MyEntity?
}Learn More: Repository-Patterns
Database Layer (Room DAOs)
Purpose: Provide type-safe SQL queries and transaction management using Room.
DAOs by Feature
Equipment DAOs
ArrowDao- Arrow queriesStabilizerDao- Stabilizer queriesSightDao- Sight queriesRiserDao- Riser queriesLimbsDao- Limbs queriesBowStringDao- Bow string queriesPlungerDao- Plunger queriesRestDao- Rest queriesAccessoryDao- Accessory queriesWeightDao- Weight queriesBowSetupDao- Bow setup queries
Scoring DAOs
RoundDao- Round queriesEndScoreDao- End score queriesArrowScoreDao- Arrow score queries
Tournament DAOs
TournamentScoreCacheDao- Score caching queries
Common DAO Pattern
@Dao
interface MyDao {
// Observable queries return Flow
@Query("SELECT * FROM my_table ORDER BY name ASC")
fun getAll(): Flow<List<MyEntity>>
// One-time queries
@Query("SELECT * FROM my_table WHERE id = :id")
suspend fun getById(id: Long): MyEntity?
// Modifications
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: MyEntity)
@Update
suspend fun update(entity: MyEntity)
@Delete
suspend fun delete(entity: MyEntity)
}Database Migrations
- Current Version: 31
- Migration Path:
Migration_30_31.kt- Added tournament score cache - See Tech-Debt for database optimization opportunities
Learn More: Database-Patterns
Service Layer (Extracted Services)
Purpose: Extract complex business logic from ViewModels to improve testability and maintainability.
Tournament Services
→ See Tournament-Services for details
Extracted from LiveScoringViewModel (2,808 → 2,304 lines):
TournamentSyncService(556 lines) - Firebase sync ✅ScoreConflictResolutionService(262 lines) - Conflict resolution ✅EndCompletionService(400 lines) - End finalization ✅TournamentRoundLifecycleService(~200 lines) - Round lifecycle ⏳ TODOStatisticsAggregationService(~150 lines) - Statistics ⏳ TODO
Pattern: See TournamentSyncService-Extraction-Plan for proven extraction pattern
Common Service Pattern
@Singleton
class MyService @Inject constructor(
private val repository: MyRepository,
private val serviceScope: CoroutineScope? = null
) {
private val scope = serviceScope ?: CoroutineScope(SupervisorJob())
// StateFlows for observable state
private val _state = MutableStateFlow(MyState())
val state: StateFlow<MyState> = _state.asStateFlow()
// Public API
suspend fun doOperation(...) { ... }
// Cleanup
fun cleanup() { ... }
}Learn More: Service-Patterns
Dependency Injection (Hilt)
All components use Hilt for dependency injection:
// ViewModels
@HiltViewModel
class MyViewModel @Inject constructor(...) : ViewModel()
// Repositories
@Singleton
class MyRepository @Inject constructor(...)
// Services
@Singleton
class MyService @Inject constructor(...)Data Flow Example
Here’s how data flows through the layers for a typical operation:
Example: Saving an Arrow
1. UI Layer (ActiveScoringScreen.kt)
User clicks "Save Arrow"
↓
2. ViewModel Layer (ArrowViewModel)
suspend fun saveArrow(): SaveResult
→ Validates input
→ Calls repository
↓
3. Repository Layer (ArrowRepository)
suspend fun insertArrow(arrow: Arrow)
→ Applies business rules
→ Calls DAO
↓
4. Database Layer (ArrowDao)
@Insert suspend fun insert(arrow: Arrow)
→ Executes SQL
→ Persists to Room database
↓
5. Observation (Flow)
fun getAll(): Flow<List<Arrow>>
← Emits updated list
↑
6. ViewModel Updates State
loadArrows() observes Flow
→ Updates _arrows StateFlow
↑
7. UI Reacts
val arrows by viewModel.arrows.collectAsState()
→ Composable recomposes with new data
Cross-Cutting Concerns
Coroutines & Flow
- ViewModelScope: Used in ViewModels for lifecycle-aware operations
- StateFlow: Exposes observable state to UI
- Flow: Used by DAOs for reactive data
Error Handling
- SaveResult Pattern: See SaveResult-Pattern
- Validation at ViewModel layer
- Try-catch at Repository layer
- User-friendly error messages to UI
Caching
- Tournament Score Cache: 5-10x performance improvement
- Status-aware TTL (30s for active, indefinite for completed)
- See Phase2-Completion-Summary
Navigation Map
-
Layer Details:
-
Feature Areas:
-
Patterns:
-
Architecture Docs:
- Architecture - Comprehensive architecture guide
- Tech-Debt - Known architectural issues
- LiveScoringVM-Analysis - God class analysis
- Checkpoint-Findings - Architecture audit findings
Last Updated: October 8, 2025 Database Version: 31 Total Lines of Code: ~50,000+ (estimated) Architecture Status: Clean Architecture with known refactoring opportunities