Overview
MiTensión uses Jetpack Compose for building declarative, reactive UI components. The app follows Material Design 3 principles with custom theming for a modern, accessible blood pressure tracking experience.
Jetpack Compose Declarative UI framework for Android
Material 3 Modern design system with dynamic colors
Custom Components Reusable composables for MiTensión
Theme Configuration
Color Scheme
MiTensión defines color palettes for light and dark themes:
// Color.kt
val Purple80 = Color ( 0xFFD0BCFF )
val PurpleGrey80 = Color ( 0xFFCCC2DC )
val Pink80 = Color ( 0xFFEFB8C8 )
val Purple40 = Color ( 0xFF6650a4 )
val PurpleGrey40 = Color ( 0xFF625b71 )
val Pink40 = Color ( 0xFF7D5260 )
private val LightColorScheme = lightColorScheme (
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
Colors:
Primary: #6650a4 (Purple 40)
Secondary: #625b71 (Purple Grey 40)
Tertiary: #7D5260 (Pink 40)
private val DarkColorScheme = darkColorScheme (
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
Colors:
Primary: #D0BCFF (Purple 80)
Secondary: #CCC2DC (Purple Grey 80)
Tertiary: #EFB8C8 (Pink 80)
Theme Composable
The main theme composable supports dynamic colors on Android 12+:
@Composable
fun MiTensionTheme (
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 = Typography,
content = content
)
}
Features:
Automatic dark/light theme detection
Dynamic color support (Android 12+)
Follows system theme preferences
Custom color schemes for older Android versions
Dynamic colors extract the color scheme from the user’s wallpaper on Android 12+.
Screen Components
MedicionScreen
The main measurement input screen:
@OptIn (ExperimentalMaterial3Api:: class )
@Composable
fun MedicionScreen (onNavigateToCalendario: () -> Unit) {
val context = LocalContext.current
val medicionDao = remember { AppDatabase. getDatabase (context). medicionDao () }
val repository = remember { MedicionRepository (medicionDao) }
val factory = remember { MedicionViewModelFactory (repository, errorViewModel) }
val viewModel: MedicionViewModel = viewModel (factory = factory)
val uiState by viewModel.uiState
var mostrarPopupSistolica by remember { mutableStateOf ( false ) }
var mostrarPopupDiastolica by remember { mutableStateOf ( false ) }
Scaffold (
topBar = {
TopAppBar (
title = { Text (tituloCompleto) },
colors = TopAppBarDefaults. topAppBarColors (
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary
)
)
}
) { paddingValues ->
Column (
modifier = Modifier
. fillMaxSize ()
. padding (paddingValues)
. padding ( 16 .dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement. spacedBy ( 24 .dp)
) {
TensionDisplay (
label = stringResource (id = R.string.tension_alta_label),
valor = uiState.sistolica,
onClick = { mostrarPopupSistolica = true }
)
TensionDisplay (
label = stringResource (id = R.string.tension_baja_label),
valor = uiState.diastolica,
onClick = { mostrarPopupDiastolica = true }
)
Spacer (modifier = Modifier. weight ( 1f ))
Row (
modifier = Modifier. fillMaxWidth (),
horizontalArrangement = Arrangement. spacedBy ( 16 .dp)
) {
Button (
onClick = { viewModel. guardarMedicion ( .. .) },
modifier = Modifier. weight ( 3f ). height ( 60 .dp)
) {
Text ( stringResource (id = R.string.guardar))
}
Button (
onClick = { onNavigateToCalendario () },
modifier = Modifier. weight ( 3f ). height ( 60 .dp)
) {
Text ( stringResource (id = R.string.ver_calendario))
}
}
}
}
}
Key Elements:
Scaffold with TopAppBar
Material 3 Scaffold provides consistent layout structure with app bar
Column Layout
Vertical arrangement with centered alignment and 24dp spacing
TensionDisplay Components
Custom composables for displaying and editing blood pressure values
Button Row
Two equally-weighted buttons for saving and navigation
TensionDisplay Component
Reusable component for displaying blood pressure values:
@Composable
fun TensionDisplay (label: String , valor: String , onClick: () -> Unit) {
Column (horizontalAlignment = Alignment.CenterHorizontally) {
Text (
text = label,
style = MaterialTheme.typography.headlineSmall
)
Spacer (modifier = Modifier. height ( 8 .dp))
Box (
modifier = Modifier
. fillMaxWidth ( 0.7f )
. height ( 120 .dp)
. background (
MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.shapes.large
)
. clickable (onClick = onClick)
. padding (horizontal = 16 .dp),
contentAlignment = Alignment.Center
) {
Text (
text = if (valor. isEmpty ())
stringResource (id = R.string.pulsa_para_anadir)
else valor,
style = MaterialTheme.typography.headlineMedium. copy (
fontWeight = FontWeight.Bold
),
color = if (valor. isEmpty ())
MaterialTheme.colorScheme.onSurfaceVariant. copy (alpha = 0.6f )
else MaterialTheme.colorScheme.primary
)
}
}
}
Component Features:
Layout
Column with centered alignment
Label text with headlineSmall style
8dp spacing between label and value box
Value Box
70% screen width
120dp fixed height
Rounded corners (large shape)
Surface variant background
Interaction
Clickable to open input dialog
Visual feedback on tap
Placeholder text when empty
Styling
Bold headline medium typography
Primary color for values
Muted color (60% alpha) for placeholder
Modal dialog for numeric input:
@Composable
fun TensionInputDialog (
titulo: String ,
valorInicial: String ,
onDismiss: () -> Unit,
onConfirm: ( String ) -> Unit
) {
var text by remember {
mutableStateOf (
TextFieldValue (
text = valorInicial,
selection = TextRange (valorInicial.length)
)
)
}
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester () }
Dialog (onDismissRequest = onDismiss) {
Card (
modifier = Modifier. fillMaxWidth (),
shape = MaterialTheme.shapes.large
) {
Column (
modifier = Modifier. padding ( 24 .dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text (titulo, style = MaterialTheme.typography.headlineSmall)
Spacer (modifier = Modifier. height ( 16 .dp))
OutlinedTextField (
value = text,
onValueChange = { newValue ->
if (newValue.text.length <= 3 && newValue.text. all { it. isDigit () }) {
text = newValue
}
},
modifier = Modifier. focusRequester (focusRequester),
keyboardOptions = KeyboardOptions (
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions (
onDone = {
onConfirm (text.text)
focusManager. clearFocus ()
}
),
singleLine = true ,
textStyle = LocalTextStyle.current. copy (textAlign = TextAlign.Center)
)
Spacer (modifier = Modifier. height ( 24 .dp))
Button (
onClick = {
onConfirm (text.text)
focusManager. clearFocus ()
},
modifier = Modifier. fillMaxWidth (). height ( 48 .dp)
) {
Text ( stringResource (id = R.string.confirmar))
}
}
}
}
LaunchedEffect (Unit) {
focusRequester. requestFocus ()
}
}
Dialog Features:
LaunchedEffect(Unit) ensures the text field receives focus when the dialog opens.
Material 3 Components
Scaffold
Provides consistent layout structure:
Scaffold (
topBar = { TopAppBar ( .. .) },
floatingActionButton = { FloatingActionButton ( .. .) },
content = { paddingValues ->
// Content here
}
)
Benefits:
Automatic padding for system bars
Consistent app bar positioning
Built-in FAB support
Material motion and elevation
TopAppBar
TopAppBar (
title = { Text ( "Medición - Mañana 1/3" ) },
colors = TopAppBarDefaults. topAppBarColors (
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary
)
)
Styling:
Primary container background
Primary color text
Material 3 elevation
Supports navigation icons and actions
Button (
onClick = { /* Action */ },
modifier = Modifier
. weight ( 3f )
. height ( 60 .dp)
) {
Text (
"Guardar" ,
style = MaterialTheme.typography.bodyLarge
)
}
Button Variants:
Filled Button Primary action button with solid background
Outlined Button Secondary action with border outline
Text Button Tertiary action with no background
OutlinedTextField
OutlinedTextField (
value = text,
onValueChange = { newValue -> text = newValue },
keyboardOptions = KeyboardOptions (
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
singleLine = true
)
Features:
Material 3 outline style
Custom keyboard types
IME action support
Label and placeholder support
Error state handling
Typography
Material 3 provides a rich typography scale:
Headlines
Body Text
Labels
Text (
text = "Tensión Alta" ,
style = MaterialTheme.typography.headlineSmall
)
Scale:
headlineLarge - 32sp
headlineMedium - 28sp
headlineSmall - 24sp
Text (
text = "Descripción" ,
style = MaterialTheme.typography.bodyLarge
)
Scale:
bodyLarge - 16sp
bodyMedium - 14sp
bodySmall - 12sp
Text (
text = "ETIQUETA" ,
style = MaterialTheme.typography.labelMedium
)
Scale:
labelLarge - 14sp
labelMedium - 12sp
labelSmall - 11sp
Layout Patterns
Column with Spacing
Column (
modifier = Modifier
. fillMaxSize ()
. padding ( 16 .dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement. spacedBy ( 24 .dp)
) {
// Children spaced 24dp apart
}
Row with Weights
Row (
modifier = Modifier. fillMaxWidth (),
horizontalArrangement = Arrangement. spacedBy ( 16 .dp)
) {
Button (
modifier = Modifier. weight ( 1f ),
onClick = { /* ... */ }
) { Text ( "Button 1" ) }
Button (
modifier = Modifier. weight ( 1f ),
onClick = { /* ... */ }
) { Text ( "Button 2" ) }
}
Box with Alignment
Box (
modifier = Modifier
. fillMaxWidth ()
. height ( 120 .dp),
contentAlignment = Alignment.Center
) {
Text ( "Centered Content" )
}
State Management
Remember and MutableState
var mostrarPopup by remember { mutableStateOf ( false ) }
if (mostrarPopup) {
TensionInputDialog (
onDismiss = { mostrarPopup = false },
onConfirm = { valor ->
viewModel. onSistolicaChanged (valor)
mostrarPopup = false
}
)
}
Collecting Flow
val uiState by viewModel.uiState
LaunchedEffect (key1 = true ) {
viewModel.evento. collectLatest { evento ->
when (evento) {
is MedicionViewModel.UiEvento.MostrarMensaje -> {
Toast. makeText (context, evento.mensaje, Toast.LENGTH_LONG). show ()
}
is MedicionViewModel.UiEvento.GuardadoConExito -> {
Toast. makeText (context, evento.mensaje, Toast.LENGTH_SHORT). show ()
viewModel. onGuardadoExitoso ()
}
}
}
}
Use collectAsState() for UI state and collectLatest in LaunchedEffect for one-time events.
Modifiers
Common modifier patterns in MiTensión:
Size Modifier
. fillMaxWidth ()
. fillMaxHeight ()
. fillMaxSize ()
. size ( 48 .dp)
. width ( 200 .dp)
. height ( 60 .dp)
Padding Modifier
. padding ( 16 .dp)
. padding (horizontal = 24 .dp)
. padding (vertical = 16 .dp)
. padding (
start = 16 .dp,
top = 8 .dp
)
Background Modifier
. background (
MaterialTheme.colorScheme.surface,
MaterialTheme.shapes.large
)
. background (Color.Red)
Interaction Modifier
. clickable { /* Action */ }
. focusRequester (focusRequester)
. focusable ()
Accessibility
Content Descriptions
Icon (
imageVector = Icons.Default.Add,
contentDescription = stringResource (id = R.string.add_measurement)
)
Semantic Properties
Modifier. semantics {
contentDescription = "Systolic blood pressure: 120"
role = Role.Button
}
Focus Management
val focusRequester = remember { FocusRequester () }
OutlinedTextField (
modifier = Modifier. focusRequester (focusRequester),
// ...
)
LaunchedEffect (Unit) {
focusRequester. requestFocus ()
}
Animation
AnimatedVisibility
AnimatedVisibility (visible = isVisible) {
Text ( "This text animates in and out" )
}
Animated Content
AnimatedContent (targetState = currentScreen) { screen ->
when (screen) {
Screen.Medicion -> MedicionScreen ()
Screen.Calendario -> CalendarioScreen ()
}
}
Best Practices
State Hoisting
Keep composables stateless when possible
Hoist state to lowest common ancestor
Pass state down, events up
Use ViewModels for business logic
Recomposition
Use remember for expensive operations
Avoid creating new objects in composition
Use key for stable identity
Profile with Layout Inspector
Resources
Use stringResource() for all text
Support multiple languages
Use dimensionResource() for sizes
Leverage string formatting
Theming
Always use MaterialTheme.colorScheme
Respect system dark mode
Use semantic color names
Test in light and dark themes
Testing Composables
UI Tests
@Test
fun testTensionDisplay_showsValue () {
composeTestRule. setContent {
MiTensionTheme {
TensionDisplay (
label = "Sistólica" ,
valor = "120" ,
onClick = {}
)
}
}
composeTestRule
. onNodeWithText ( "120" )
. assertIsDisplayed ()
}
@Test
fun testTensionDisplay_clickable () {
var clicked = false
composeTestRule. setContent {
TensionDisplay (
label = "Sistólica" ,
valor = "120" ,
onClick = { clicked = true }
)
}
composeTestRule
. onNodeWithText ( "120" )
. performClick ()
assertTrue (clicked)
}
Use derivedStateOf
For computed values that depend on other state: val isValid by remember {
derivedStateOf {
sistolica. isNotBlank () && diastolica. isNotBlank ()
}
}
Avoid Unnecessary Recomposition
Use stable types and avoid lambdas with captures: @Stable
data class ButtonState ( val enabled: Boolean , val text: String )
Use LazyColumn for Lists
Instead of Column with many items: LazyColumn {
items (mediciones) { medicion ->
MedicionItem (medicion)
}
}
Architecture Learn about the MVVM architecture pattern
Data Model Explore the Room database schema