android-reports

Installation
SKILL.md

Android Mobile Reports

SUPERSEDED: This skill has been replaced by mobile-reports, which covers both Android (Jetpack Compose) and iOS (SwiftUI). Use mobile-reports for all new work. This file is retained for backward compatibility with existing projects.

Required Plugins

Superpowers plugin: MUST be active for all work using this skill. Use throughout the entire build pipeline — design decisions, code generation, debugging, quality checks, and any task where it offers enhanced capabilities. If superpowers provides a better way to accomplish something, prefer it over the default approach.

Overview

Mobile reports require different design considerations than desktop reports due to screen size, touch interactions, and usage patterns. This skill provides proven patterns for creating effective, readable, and performant report experiences in Android apps using Jetpack Compose and Material 3.

Android 10+ required.

Icon Policy: Use custom PNG icons only. Use painterResource(R.drawable.<name>) placeholders and update PROJECT_ICONS.md (see android-custom-icons).

Report Table Policy: If a report can exceed 25 rows, it must use a table layout (see android-report-tables).

Core Principles

1. Responsive Layout with Relative Positioning

  • Use fillMaxWidth(), weight(), and percentage-based sizing instead of fixed dp values
  • Design layouts that adapt to both portrait and landscape orientations
  • Use LocalConfiguration.current.screenWidthDp to adjust columns/layout based on device size
  • Prefer LazyColumn/LazyRow for scrollable content over fixed containers

2. Progressive Disclosure

  • Show summary/overview first, details on demand
  • Use expandable cards or drill-down navigation for detailed data
  • Implement infinite scroll pagination for large datasets
  • Load data progressively (initial view fast, details on interaction)

3. Touch-Friendly Interactions

  • Minimum tap target size: 48dp (Material 3 standard)
  • Use bottom sheets for filters and actions (thumb-reachable)
  • Implement swipe gestures for common actions (refresh, delete)
  • Provide visual feedback for all interactions (ripple, state changes)

4. Readable Typography

  • Minimum font size: 14sp for body text, 16sp preferred
  • Line spacing: 1.5x line height for readability
  • Contrast: Ensure WCAG AA compliance (4.5:1 for normal text)
  • Limit text column width for comfortable reading (avoid full-width text on tablets)

5. Data Visualization

  • Charts: Use Vico only (Kotlin-first, Compose-friendly, and actively maintained)
  • Tables: Limit to 3-4 columns on phone, 5-6 on tablet
  • Avoid nested tables - use grouped sections or pagination instead
  • Color coding: Use color sparingly, ensure accessibility (not color-only indicators)

6. Performance Optimization

  • Lazy load data with pagination (offset or cursor-based)
  • Cache rendered reports/charts (remember/derivedStateOf)
  • Use key() in LazyColumn for efficient recomposition
  • Defer expensive calculations to background coroutines

Report Layout Patterns

Pattern 1: Summary Card + Detail List

@Composable
fun InventoryReportScreen(state: ReportState) {
    LazyColumn {
        // Summary section (always visible)
        item {
            SummaryCard(
                totalValue = state.totalValue,
                itemCount = state.itemCount,
                lowStock = state.lowStockCount
            )
        }

        // Filter bar (sticky)
        stickyHeader {
            FilterBar(
                selectedCategory = state.filter,
                onFilterChange = { /* ... */ }
            )
        }

        // Detail items (paginated)
        items(state.items, key = { it.id }) { item ->
            ReportItemCard(item)
        }

        // Load more trigger
        if (state.hasMore) {
            item {
                LoadMoreTrigger(onLoadMore = { /* ... */ })
            }
        }
    }
}

Pattern 1b: Table-First Paginated Report (25+ Rows)

Use this when the report can exceed 25 rows. Table with sticky header and floating footer.

@Composable
fun PaginatedReportScreen(allData: List<ReportItem>) {
    val pageSize = 25
    var currentPage by remember { mutableIntStateOf(1) }
    val listState = rememberLazyListState()

    val totalPages = maxOf(1, ceil(allData.size.toDouble() / pageSize).toInt())
    val pagedItems = remember(currentPage, allData) {
        val start = (currentPage - 1) * pageSize
        val end = minOf(start + pageSize, allData.size)
        allData.subList(start, end)
    }

    LaunchedEffect(currentPage) {
        listState.animateScrollToItem(0)
    }

    Scaffold(
        bottomBar = {
            Column {
                HorizontalDivider(
                    thickness = 0.5.dp,
                    color = MaterialTheme.colorScheme.outlineVariant
                )
                TablePaginationController(
                    currentPage = currentPage,
                    totalPages = totalPages,
                    onPageChange = { currentPage = it }
                )
            }
        }
    ) { paddingValues ->
        LazyColumn(
            state = listState,
            modifier = Modifier.padding(paddingValues).fillMaxSize()
        ) {
            stickyHeader {
                ReportRow(
                    data = listOf("ID", "Customer", "Amount"),
                    isHeader = true,
                    weights = listOf(0.2f, 0.5f, 0.3f)
                )
            }

            items(pagedItems) { item ->
                ReportRow(
                    data = listOf(item.id, item.name, item.amount),
                    weights = listOf(0.2f, 0.5f, 0.3f)
                )
            }
        }
    }
}

Pattern 2: Tab-Based Multi-Report

@Composable
fun ReportsScreen() {
    var selectedTab by remember { mutableStateOf(0) }
    val tabs = listOf("Sales", "Inventory", "Customers")

    Column {
        TabRow(selectedTabIndex = selectedTab) {
            tabs.forEachIndexed { index, title ->
                Tab(
                    selected = selectedTab == index,
                    onClick = { selectedTab = index },
                    text = { Text(title) }
                )
            }
        }

        when (selectedTab) {
            0 -> SalesReportContent()
            1 -> InventoryReportContent()
            2 -> CustomersReportContent()
        }
    }
}

Pattern 3: Filter Bottom Sheet + Results

@Composable
fun FilterableReportScreen(viewModel: ReportViewModel) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    var showFilters by remember { mutableStateOf(false) }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Sales Report") },
                actions = {
                    IconButton(onClick = { showFilters = true }) {
                        Icon(painterResource(R.drawable.filter), "Filters")
                    }
                }
            )
        }
    ) { padding ->
        ReportContent(
            data = state.data,
            modifier = Modifier.padding(padding)
        )
    }

    if (showFilters) {
        ModalBottomSheet(onDismissRequest = { showFilters = false }) {
            FilterForm(
                currentFilters = state.filters,
                onApply = { filters ->
                    viewModel.applyFilters(filters)
                    showFilters = false
                }
            )
        }
    }
}

Interactive Filtering

Date Range Selection

@Composable
fun DateRangeFilter(
    startDate: LocalDate,
    endDate: LocalDate,
    onRangeChange: (LocalDate, LocalDate) -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        OutlinedButton(
            onClick = { /* Show date picker */ },
            modifier = Modifier.weight(1f)
        ) {
            Text("From: ${startDate.format()}")
        }
        OutlinedButton(
            onClick = { /* Show date picker */ },
            modifier = Modifier.weight(1f)
        ) {
            Text("To: ${endDate.format()}")
        }
    }
}

Quick Filters (Chips)

@Composable
fun QuickFilters(
    options: List<FilterOption>,
    selected: FilterOption?,
    onSelect: (FilterOption) -> Unit
) {
    LazyRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(horizontal = 16.dp)
    ) {
        items(options) { option ->
            FilterChip(
                selected = option == selected,
                onClick = { onSelect(option) },
                label = { Text(option.label) }
            )
        }
    }
}

Table Design for Mobile

Avoid This (Too Many Columns)

| SKU | Name | Category | Qty | Unit | Value | Location | Status |

Do This Instead (Card-Based)

@Composable
fun StockItemCard(item: StockItem) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            // Primary info
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(
                    text = item.name,
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.SemiBold
                )
                Text(
                    text = formatCurrency(item.value),
                    style = MaterialTheme.typography.titleMedium,
                    color = MaterialTheme.colorScheme.primary
                )
            }

            Spacer(modifier = Modifier.height(8.dp))

            // Secondary info (grid)
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                InfoItem("Qty", item.quantity.toString())
                InfoItem("Category", item.category)
                InfoItem("Location", item.warehouse)
            }

            // Status badge
            StockStatusBadge(item.status)
        }
    }
}

@Composable
private fun InfoItem(label: String, value: String) {
    Column {
        Text(
            text = label,
            style = MaterialTheme.typography.labelSmall,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
        Text(
            text = value,
            style = MaterialTheme.typography.bodyMedium
        )
    }
}

Export Functionality

Export Format Selection

PDF: Best for sharing/printing formatted reports (use Android-PDF-Writer or similar) CSV: Best for data analysis in spreadsheets Share: Best for immediate sharing via messaging apps

@Composable
fun ExportMenu(
    onExportPdf: () -> Unit,
    onExportCsv: () -> Unit,
    onShare: () -> Unit
) {
    var expanded by remember { mutableStateOf(false) }

    IconButton(onClick = { expanded = true }) {
        Icon(painterResource(R.drawable.share), "Export")
    }

    DropdownMenu(
        expanded = expanded,
        onDismissRequest = { expanded = false }
    ) {
        DropdownMenuItem(
            text = { Text("Export as PDF") },
            leadingIcon = { Icon(painterResource(R.drawable.pdf), null) },
            onClick = {
                expanded = false
                onExportPdf()
            }
        )
        DropdownMenuItem(
            text = { Text("Export as CSV") },
            leadingIcon = { Icon(painterResource(R.drawable.table), null) },
            onClick = {
                expanded = false
                onExportCsv()
            }
        )
        DropdownMenuItem(
            text = { Text("Share Report") },
            leadingIcon = { Icon(painterResource(R.drawable.send), null) },
            onClick = {
                expanded = false
                onShare()
            }
        )
    }
}

Chart Integration

Using Vico (Required)

Vico is our standard charting library for business apps.

  • 100% Kotlin, works with Jetpack Compose and the View system
  • Compose Multiplatform support for future sharing
  • Extensible, professional-grade charts and interactions
  • Actively maintained with a strong release cadence

Implementation checklist:

  • Read the official guide at guide.vico.patrykandpatrick.com for setup
  • Use the Compose artifact for new screens; Views only for legacy screens
  • Start from the Vico sample module to mirror production patterns
@Composable
fun SalesChart(data: List<SalesDataPoint>) {
    val chartEntryModel = entryModelOf(
        data.mapIndexed { index, point ->
            entryOf(index, point.amount)
        }
    )

    Card(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "Sales Trend",
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.SemiBold
            )
            Spacer(modifier = Modifier.height(8.dp))
            Chart(
                chart = lineChart(),
                model = chartEntryModel,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(200.dp)
            )
        }
    }
}

Loading States

@Composable
fun ReportLoadingState() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        CircularProgressIndicator()
        Spacer(modifier = Modifier.height(16.dp))
        Text(
            text = "Generating report...",
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}

Empty States

@Composable
fun ReportEmptyState(
    message: String = "No data for the selected period",
    action: (@Composable () -> Unit)? = null
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Icon(

---
> **Note:** Content trimmed to 500-line standard. Move overflow content to `references/` for on-demand loading.
Related skills
Installs
2
GitHub Stars
12
First Seen
Mar 30, 2026