RoundAnalyticsViewModel API Reference

Complete API reference for the RoundAnalyticsViewModel - managing analytics and statistics UI state.


Overview

File: ui/viewmodels/RoundAnalyticsViewModel.kt Lines: 605 lines Status: ✅ Production | 📝 Needs comprehensive documentation

Purpose

RoundAnalyticsViewModel manages UI state for round analytics and performance statistics, providing:

  • Score statistics and aggregations
  • Performance trends over time
  • Equipment correlation analysis
  • Distribution charts and graphs
  • Comparative analytics
  • Historical performance tracking

UI State

data class AnalyticsUiState(
    val roundId: Long?,
    val isLoading: Boolean = false,
    val error: String? = null,
 
    // Basic stats
    val totalScore: Int = 0,
    val averagePerEnd: Double = 0.0,
    val averagePerArrow: Double = 0.0,
    val xCount: Int = 0,
    val xCountPercentage: Double = 0.0,
 
    // Distribution
    val scoreDistribution: Map<Int, Int> = emptyMap(),
    val endScores: List<Int> = emptyList(),
 
    // Trends
    val performanceTrend: TrendDirection = TrendDirection.STABLE,
    val consistencyScore: Double = 0.0,
 
    // Equipment
    val bowSetup: BowSetup? = null,
    val equipmentPerformance: EquipmentPerformanceStats? = null,
 
    // Comparisons
    val personalBest: Int? = null,
    val averageComparison: Double = 0.0  // vs user average
)

Key Methods

1. Load Analytics

fun loadAnalytics(roundId: Long)

Purpose: Loads comprehensive analytics for a round

Example:

class RoundAnalyticsScreen(viewModel: RoundAnalyticsViewModel) {
    LaunchedEffect(roundId) {
        viewModel.loadAnalytics(roundId)
    }
 
    val state by viewModel.uiState.collectAsState()
 
    if (state.isLoading) {
        LoadingIndicator()
    } else {
        AnalyticsContent(state)
    }
}

2. Refresh Analytics

fun refreshAnalytics()

Purpose: Refreshes analytics data

Example:

PullToRefresh(
    onRefresh = { viewModel.refreshAnalytics() }
) {
    AnalyticsContent()
}

Analytics Displays

Score Distribution Chart

@Composable
fun ScoreDistributionChart(
    distribution: Map<Int, Int>,
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier) {
        distribution.toSortedMap(reverseOrder()).forEach { (score, count) ->
            Row {
                Text("$score", modifier = Modifier.width(24.dp))
                LinearProgressIndicator(
                    progress = count / distribution.values.max().toFloat(),
                    modifier = Modifier.weight(1f)
                )
                Text("$count", modifier = Modifier.width(32.dp))
            }
        }
    }
}

End Scores Trend Line

@Composable
fun EndScoresTrendChart(
    endScores: List<Int>,
    modifier: Modifier = Modifier
) {
    val average = endScores.average()
 
    Canvas(modifier = modifier.fillMaxWidth().height(200.dp)) {
        // Draw average line
        drawLine(
            color = Color.Gray,
            start = Offset(0f, size.height / 2),
            end = Offset(size.width, size.height / 2),
            strokeWidth = 2.dp.toPx()
        )
 
        // Draw end scores as connected points
        val points = endScores.mapIndexed { index, score ->
            val x = (index / endScores.size.toFloat()) * size.width
            val y = size.height * (1 - (score / 60f))  // Normalize to 0-60
            Offset(x, y)
        }
 
        points.zipWithNext().forEach { (p1, p2) ->
            drawLine(
                color = Color.Blue,
                start = p1,
                end = p2,
                strokeWidth = 3.dp.toPx()
            )
        }
 
        // Draw points
        points.forEach { point ->
            drawCircle(
                color = Color.Blue,
                radius = 4.dp.toPx(),
                center = point
            )
        }
    }
}

Usage Examples

Complete Analytics Screen

@Composable
fun RoundAnalyticsScreen(
    roundId: Long,
    viewModel: RoundAnalyticsViewModel = viewModel()
) {
    LaunchedEffect(roundId) {
        viewModel.loadAnalytics(roundId)
    }
 
    val state by viewModel.uiState.collectAsState()
 
    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Analytics") })
        }
    ) { padding ->
        if (state.isLoading) {
            LoadingIndicator()
        } else {
            LazyColumn(modifier = Modifier.padding(padding)) {
                // Summary stats
                item {
                    StatsCard(
                        totalScore = state.totalScore,
                        averagePerEnd = state.averagePerEnd,
                        averagePerArrow = state.averagePerArrow,
                        xCount = state.xCount
                    )
                }
 
                // Score distribution
                item {
                    Card {
                        Text("Score Distribution", style = MaterialTheme.typography.h6)
                        ScoreDistributionChart(state.scoreDistribution)
                    }
                }
 
                // End scores trend
                item {
                    Card {
                        Text("End Scores", style = MaterialTheme.typography.h6)
                        EndScoresTrendChart(state.endScores)
                    }
                }
 
                // Equipment performance
                state.equipmentPerformance?.let { equipPerf ->
                    item {
                        EquipmentPerformanceCard(equipPerf)
                    }
                }
 
                // Comparison
                state.personalBest?.let { pb ->
                    item {
                        ComparisonCard(
                            currentScore = state.totalScore,
                            personalBest = pb,
                            averageComparison = state.averageComparison
                        )
                    }
                }
            }
        }
    }
}

Testing

Unit Test Example

class RoundAnalyticsViewModelTest {
    private lateinit var viewModel: RoundAnalyticsViewModel
    private lateinit var mockRepository: RoundRepository
    private lateinit var mockStatsService: StatisticsCalculationService
 
    @Before
    fun setup() {
        mockRepository = mockk()
        mockStatsService = mockk()
        viewModel = RoundAnalyticsViewModel(mockRepository, mockStatsService)
    }
 
    @Test
    fun `loadAnalytics updates state with statistics`() = runTest {
        // Arrange
        val roundId = 123L
        val endScores = List(10) { EndScore(totalScore = 54 + it) }
 
        coEvery { mockRepository.getEndScoresForRound(roundId) } returns
            Result.success(endScores)
        every { mockStatsService.calculateRoundTotal(endScores) } returns 545
        every { mockStatsService.calculateRoundAverage(endScores) } returns 54.5
 
        // Act
        viewModel.loadAnalytics(roundId)
        advanceUntilIdle()
 
        // Assert
        val state = viewModel.uiState.value
        assertEquals(545, state.totalScore)
        assertEquals(54.5, state.averagePerEnd, 0.01)
        assertFalse(state.isLoading)
    }
}

Architecture:

Related Components:

Flows:


Status: ✅ Production | 📝 Needs comprehensive documentation Lines: 605 lines Last Updated: 2025-11-01