mobile-reports/SKILL.md
Best practices for designing mobile-optimized reports in Android (Jetpack Compose) and iOS (SwiftUI) apps. Use when implementing report screens, data visualization, export functionality, or any feature that displays aggregated data, analytics, or...
npx skillsauth add peterbamuhigire/skills-web-dev mobile-reportsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
mobile-reports or would be better handled by a more specific companion skill.references only as needed.SKILL.md first, then load only the referenced deep-dive files that are necessary for the task.references/ directory for deep detail after reading the core workflow below.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 (Jetpack Compose / Material 3) and iOS (SwiftUI).
painterResource(R.drawable.<name>)).android-report-tables).LazyColumn/LazyRow (Android) or List/ScrollView (iOS) for scrollable contentremember/derivedStateOf on Android; @State/computed on iOS)Android (Jetpack Compose):
@Composable
fun InventoryReportScreen(state: ReportState) {
LazyColumn {
item { SummaryCard(state.totalValue, state.itemCount, state.lowStockCount) }
stickyHeader { FilterBar(state.filter, onFilterChange = { /* ... */ }) }
items(state.items, key = { it.id }) { item -> ReportItemCard(item) }
if (state.hasMore) {
item { LoadMoreTrigger(onLoadMore = { /* ... */ }) }
}
}
}
iOS (SwiftUI):
struct InventoryReportView: View {
@StateObject private var viewModel = ReportViewModel()
var body: some View {
List {
Section { SummaryCard(state: viewModel.summary) }
Section {
ForEach(viewModel.items) { item in
ReportItemRow(item: item)
}
}
}
.refreshable { await viewModel.reload() }
.searchable(text: $viewModel.searchText)
}
}
Use 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 totalPages = maxOf(1, ceil(allData.size.toDouble() / pageSize).toInt())
val pagedItems = remember(currentPage, allData) {
val start = (currentPage - 1) * pageSize
allData.subList(start, minOf(start + pageSize, allData.size))
}
Scaffold(
bottomBar = { TablePaginationController(currentPage, totalPages) { currentPage = it } }
) { padding ->
LazyColumn(modifier = Modifier.padding(padding).fillMaxSize()) {
stickyHeader {
ReportRow(listOf("ID", "Customer", "Amount"), isHeader = true, weights = listOf(0.2f, 0.5f, 0.3f))
}
items(pagedItems) { item ->
ReportRow(listOf(item.id, item.name, item.amount), weights = listOf(0.2f, 0.5f, 0.3f))
}
}
}
}
On iOS, use a List with .listStyle(.plain) and a custom pagination bar in a VStack footer.
Android:
@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()
}
}
}
iOS:
struct ReportsView: View {
var body: some View {
TabView {
SalesReportContent().tabItem { Label("Sales", image: "sales") }
InventoryReportContent().tabItem { Label("Inventory", image: "inventory") }
CustomersReportContent().tabItem { Label("Customers", image: "customers") }
}
}
}
Android:
@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 = { viewModel.applyFilters(it); showFilters = false })
}
}
}
iOS:
struct FilterableReportView: View {
@StateObject private var viewModel = ReportViewModel()
@State private var showFilters = false
var body: some View {
NavigationStack {
ReportContent(data: viewModel.data)
.toolbar {
Button { showFilters = true } label: { Image("filter") }
}
.sheet(isPresented: $showFilters) {
FilterForm(filters: $viewModel.filters)
}
}
}
}
@Composable
fun DateRangeFilter(startDate: LocalDate, endDate: LocalDate, onRangeChange: (LocalDate, LocalDate) -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { /* date picker */ }, modifier = Modifier.weight(1f)) { Text("From: ${startDate.format()}") }
OutlinedButton(onClick = { /* date picker */ }, modifier = Modifier.weight(1f)) { Text("To: ${endDate.format()}") }
}
}
On iOS, use DatePicker with .datePickerStyle(.compact) in a similar two-column layout.
@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) })
}
}
}
On iOS, use a horizontal ScrollView with Toggle buttons or a segmented Picker.
| SKU | Name | Category | Qty | Unit | Value | Location | Status |
Use card-based layouts that show primary info (name + value) prominently, secondary info (qty, category, location) in a grid row below, and status as a badge. This adapts naturally to any screen width. See Pattern 1 for the structural approach.
Formats: PDF (sharing/printing), CSV (spreadsheet analysis), Share (messaging apps).
Android: Use a DropdownMenu with export options triggered from the TopAppBar actions area.
@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") }, onClick = { expanded = false; onExportPdf() })
DropdownMenuItem(text = { Text("Export as CSV") }, onClick = { expanded = false; onExportCsv() })
DropdownMenuItem(text = { Text("Share Report") }, onClick = { expanded = false; onShare() })
}
}
iOS: Use UIActivityViewController (via ShareLink in SwiftUI) for the native share sheet. For PDF generation, use UIGraphicsPDFRenderer.
guide.vico.patrykandpatrick.com. Use the Compose artifact for new screens.Both libraries support line, bar, area, and pie charts suitable for business reports.
Provide clear feedback for all report states. Both platforms need:
Android: Use CircularProgressIndicator and composable empty/error screens.
iOS: Use ProgressView and ContentUnavailableView (iOS 17+) or a custom empty view.
LazyColumn in LazyColumn / List in ScrollView)fillMaxWidth, weight / .frame(maxWidth: .infinity))Android:
presentation/reports/
├── screens/ # ReportListScreen, SalesReportScreen, InventoryReportScreen
├── viewmodels/ # SalesReportViewModel, InventoryReportViewModel
├── components/ # SummaryCard, FilterBottomSheet, DateRangeSelector, ExportMenu, ReportChart
└── export/ # PdfExporter, CsvExporter
domain/usecase/reports/ # GetSalesReportUseCase, ExportReportUseCase
data/repository/ # ReportsRepositoryImpl (API + caching)
iOS:
Features/Reports/
├── Views/ # ReportListView, SalesReportView, InventoryReportView
├── ViewModels/ # SalesReportViewModel, InventoryReportViewModel
├── Components/ # SummaryCard, FilterSheet, ReportChart
└── Services/ # PdfExporter, CsvExporter
See references/report-types.md for detailed patterns for:
LazyColumn / List)remember/derivedStateOf / @State/computed)Dispatchers.IO / Task { })key() / id:)data-ai
Use when adding AI-powered analytics to a SaaS platform — semantic search over business data, natural language queries, trend detection, anomaly alerts, and AI-generated insights for dashboards. Covers embeddings, NL2SQL, and per-tenant analytics...
data-ai
Design AI-powered analytics dashboards — what metrics to show, how to display AI predictions and confidence, drill-down patterns, KPI cards, trend visualisation, AI Insights panels, export design, and role-based dashboard variants. Invoke when...
development
Use when designing, building, reviewing, or upgrading production software systems that must be secure, performant, maintainable, scalable, and user-centered. Apply before writing specs, code, architecture, APIs, databases, mobile apps, SaaS platforms, or ERP systems.
development
Professional web app UI using commercial templates (Tabler/Bootstrap 5) with strong frontend design direction when needed. Use for CRUD interfaces, dashboards, admin panels with SweetAlert2, DataTables, Flatpickr. Clone seeder-page.php, use...