01 — Color System
Paleta Material You — Tema verde agrícola
☀️ Light Theme
Primary
#2E7D32
#2E7D32
PrimaryContainer
#B7E4B9
#B7E4B9
Secondary
#52634F
#52634F
SecondaryContainer
#D5E8D0
#D5E8D0
Tertiary
#38656A
#38656A
TertiaryContainer
#BCEBF0
#BCEBF0
Surface
#F8FAF7
#F8FAF7
SurfaceVariant
#DCE5D9
#DCE5D9
Error
#BA1A1A
#BA1A1A
Outline
#717970
#717970
🌙 Dark Theme
Primary
#6DBF70
#6DBF70
PrimaryContainer
#1A3D1C
#1A3D1C
Secondary
#BACBB5
#BACBB5
SecondaryContainer
#2E3D2A
#2E3D2A
Tertiary
#A0CFD4
#A0CFD4
TertiaryContainer
#1A3E44
#1A3E44
Surface
#1C1F1A
#1C1F1A
SurfaceVariant
#2B2F2A
#2B2F2A
Error
#FFB4AB
#FFB4AB
Outline
#8A9E87
#8A9E87
02 — Typography
Material 3 Type Scale
Parcelas del Norte
57sp / 64sp
Finca La Vega
28sp / 36sp
Tratamiento fitosanitario #247
22sp / 28sp
Registro de incidencia
16sp / 24sp · M500
Presencia de Botrytis cinerea en sector 3 — requiere tratamiento preventivo con azufre.
16sp / 24sp
Última actualización: hoy a las 14:32 · Técnico: Marta Rodríguez
14sp / 20sp
ESTADO · CRÍTICO · VER MÁS
12sp · M500
03 — Componentes
Material 3 Component Library
Buttons
➕
Chips — Estado parcelas
Óptimo
Riesgo
Crítico
Filter chips
✓ Activas
En tratamiento
Cosecha próxima
🌿 Ecológico
Navigation Bar + Top App Bar
— contenido —
Text Fields
Máx. 80 caracteres
⚠ Este campo es obligatorio
04 — Pantallas de referencia
Flujo principal: Dashboard → Parcela → Incidencia
9:41
📶 📡 🔋
Buenos días,
Marta Rodríguez 👋
12
Parcelas activas
3
Con alertas
48h
Próx. lluvia
Parcelas recientes
Ver todas
🍇
La Vega — Sector 3
4.2 ha · Tempranillo
Óptimo
🌾
Cereal Norte — P12
8.7 ha · Trigo blando
Riesgo
🫒
Olivos Extremadura
11.3 ha · Picual
Crítico
➕
🌤 Dashboard — Light Mode
9:41
📶 📡 🔋
Parcela afectada
Olivos Extremadura — P07
Tipo de incidencia
🦠 Plaga
💧 Riego
🌡 Clima
⚙️ Maquinaria
Severidad
🟢 Baja
🟡 Media
🔴 Alta
Añadir foto
JPG · PNG · máx 10 MB
Descripción
Presencia de Bactrocera oleae en…
🌙 Formulario Incidencia — Dark Mode
9:41
📶 📡 🔋
🗺
📍 38.7789°N, 6.2134°W
Olivos Extremadura
Parcela 07 · Badajoz, Extremadura
Crítico
🫒 Picual
🌿 Ecológico
Superficie
11,3 ha
Cosecha prev.
Oct 2026
Incidencias
3 abiertas
Últ. tratam.
hace 5d
Incidencias activas
Ver historial
🦠 Bactrocera oleae — Alta
Registrado hace 2h · Sector sur
📋
🌤 Detalle Parcela — Light Mode
05 — Código Kotlin / Compose
Implementación de referencia
AgroSyncTheme.kt — Material 3 Theme Setup
private val LightColorScheme = lightColorScheme( primary = Color(0xFF2E7D32), onPrimary = Color(0xFFFFFFFF), primaryContainer = Color(0xFFB7E4B9), onPrimaryContainer = Color(0xFF002105), secondary = Color(0xFF52634F), secondaryContainer = Color(0xFFD5E8D0), tertiary = Color(0xFF38656A), surface = Color(0xFFF8FAF7), surfaceVariant = Color(0xFFDCE5D9), error = Color(0xFFBA1A1A), ) @Composable fun AgroSyncTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> DarkColorScheme else -> LightColorScheme } MaterialTheme( colorScheme = colorScheme, typography = AgroSyncTypography, content = content ) }
ParcelaCard.kt — Composable con estado
@Composable fun ParcelaCard( parcela: Parcela, onParcelaClick: (String) -> Unit, modifier: Modifier = Modifier ) { val statusColor = when (parcela.status) { ParcelaStatus.OPTIMO -> MaterialTheme.colorScheme.primaryContainer ParcelaStatus.RIESGO -> Color(0xFFFFF3E0) ParcelaStatus.CRITICO -> MaterialTheme.colorScheme.errorContainer } Card( onClick = { onParcelaClick(parcela.id) }, modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ), elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) ) { Row( modifier = Modifier.padding(14.dp).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(44.dp) .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center ) { Text(parcela.cropEmoji, fontSize = 20.sp) } Spacer(Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = parcela.name, style = MaterialTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = "${parcela.hectareas} ha · ${parcela.cultivo}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } ParcelaStatusChip(status = parcela.status) } } } @Preview(name = "Light", showBackground = true) @Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ParcelaCardPreview() { AgroSyncTheme { ParcelaCard( parcela = sampleParcela(), onParcelaClick = {} ) } }
MainScreen.kt — Navigation Compose con Bottom Bar
@Composable fun MainScreen() { val navController = rememberNavController() Scaffold( bottomBar = { NavigationBar( containerColor = MaterialTheme.colorScheme.surface, tonalElevation = 3.dp ) { val currentDest by navController.currentBackStackEntryAsState() AgroSyncDestination.entries.forEach { dest -> NavigationBarItem( icon = { Icon(dest.icon, contentDescription = stringResource(dest.labelRes)) }, label = { Text(stringResource(dest.labelRes)) }, selected = currentDest?.destination?.hierarchy?.any { it.route == dest.route } == true, onClick = { navController.navigate(dest.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } ) } } }, floatingActionButton = { ExtendedFloatingActionButton( onClick = { navController.navigate(Route.NuevaIncidencia) }, containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) { Icon(Icons.Default.Add, contentDescription = null) Spacer(Modifier.width(8.dp)) Text("Nueva incidencia") } } ) { padding -> NavHost( navController = navController, startDestination = AgroSyncDestination.Home.route, modifier = Modifier.padding(padding) ) { composable(AgroSyncDestination.Home.route) { HomeScreen(navController) } composable(AgroSyncDestination.Parcelas.route) { ParcelasScreen(navController) } composable(AgroSyncDestination.Alertas.route) { AlertasScreen() } composable(AgroSyncDestination.Perfil.route) { PerfilScreen() } composable("parcela/{id}") { backStack -> ParcelaDetailScreen( parcelaId = backStack.arguments?.getString("id")!!, onBack = { navController.popBackStack() } ) } } } }
06 — Accesibilidad & Best Practices
Checklist WCAG AA + Material You
-
Touch targets mínimo 48×48 dp Todos los elementos interactivos (botones, items de nav, iconos) cumplen el mínimo requerido por Material 3.
-
Contraste de color 4.5:1 (texto normal) / 3:1 (texto grande) Paleta verificada: verde primario #2E7D32 sobre blanco = ratio 5.14:1 ✓
-
contentDescription en todos los elementos interactivos Icons, imágenes y botones sin texto tienen descripciones para TalkBack.
-
State hoisting — composables stateless cuando sea posible Estado elevado al ViewModel (MVVM), composables reciben lambdas de callback.
-
rememberSaveable para estado de UI que sobrevive rotación Formularios de incidencia usan rememberSaveable para no perder datos al rotar.
-
LazyColumn/LazyVerticalGrid para listas largas Evita Column/Row con muchos hijos. Usa key{} para optimizar recomposiciones.
-
Dynamic color (Material You) habilitado en Android 12+ La app adapta su tema al fondo de pantalla del usuario. Fallback a paleta verde fija en Android <12.
-
WindowSizeClass para layouts adaptativos Compact → Bottom Nav. Medium → Navigation Rail. Expanded → Navigation Drawer.
-
@Preview con múltiples configuraciones Cada composable tiene preview Light, Dark, Large Font y RTL para validación rápida.