Skip to main content

Overview

MiTensión follows the MVVM (Model-View-ViewModel) architecture pattern, which provides clear separation of concerns and makes the codebase maintainable, testable, and scalable.

Model

Data layer with Room database, entities, DAOs, and repositories

View

UI layer built with Jetpack Compose components

ViewModel

Business logic that connects Model and View using Kotlin Flow

Architecture Layers

1

Data Layer (Model)

The data layer manages the app’s data sources and business entities.Components:
  • AppDatabase - Room database singleton
  • Medicion - Entity representing blood pressure measurements
  • MedicionDao - Data Access Object for database queries
  • MedicionRepository - Abstraction layer between data sources and ViewModels
@Database(entities = [Medicion::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun medicionDao(): MedicionDao
    
    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        
        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "mitension_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}
The database uses the singleton pattern to ensure only one instance exists throughout the app lifecycle.
2

Repository Pattern

The repository acts as a single source of truth and abstracts data sources from the ViewModel.
class MedicionRepository(private val medicionDao: MedicionDao) {
    
    suspend fun insertarMedicion(medicion: Medicion) {
        medicionDao.insertar(medicion)
    }
    
    suspend fun contarMedicionesEnRango(inicio: Long, fin: Long): Int {
        return medicionDao.contarMedicionesEnRango(inicio, fin)
    }
    
    fun obtenerMedicionesEnRango(inicio: Long, fin: Long): Flow<List<Medicion>> {
        return medicionDao.obtenerMedicionesPorDia(inicio, fin)
    }
    
    fun obtenerResumenMensual(inicioDelMes: Long, finDelMes: Long): Flow<List<ResumenDiario>> {
        return medicionDao.obtenerResumenMensual(inicioDelMes, finDelMes)
    }
}
Benefits:
  • Decouples data sources from business logic
  • Makes unit testing easier
  • Provides a clean API for ViewModels
  • Allows for future data source changes without affecting ViewModels
3

ViewModel Layer

ViewModels hold UI state and handle business logic. They survive configuration changes (like screen rotations).
data class MedicionUiState(
    val sistolica: String = "",
    val diastolica: String = "",
    val periodo: PeriodoDelDia = obtenerPeriodoActual(),
    val numeroMedicion: Int = 1
)

class MedicionViewModel(private val repository: MedicionRepository) : ViewModel() {
    
    private val _uiState = mutableStateOf(MedicionUiState())
    val uiState: State<MedicionUiState> = _uiState
    
    private val _evento = MutableSharedFlow<UiEvento>()
    val evento = _evento.asSharedFlow()
    
    fun guardarMedicion(mensajeErrorCampos: String, 
                       mensajeErrorPeriodoLleno: String, 
                       mensajeExito: String) {
        viewModelScope.launch {
            // Validation and business logic
            val nuevaMedicion = Medicion(
                sistolica = _uiState.value.sistolica.toInt(),
                diastolica = _uiState.value.diastolica.toInt()
            )
            repository.insertarMedicion(nuevaMedicion)
            _evento.emit(UiEvento.GuardadoConExito(mensajeExito))
        }
    }
}
ViewModels use viewModelScope for coroutine operations, which automatically cancels when the ViewModel is cleared.
4

View Layer (Jetpack Compose)

The UI is built with declarative Jetpack Compose components that react to state changes.
@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
    
    // UI reacts to state changes
    TensionDisplay(
        label = stringResource(id = R.string.tension_alta_label),
        valor = uiState.sistolica,
        onClick = { /* Show dialog */ }
    )
}
Key Principles:
  • UI is a function of state
  • Unidirectional data flow
  • State hoisting for reusable components

Data Flow

The data flows through the architecture in a unidirectional manner:
  1. User enters blood pressure values in MedicionScreen
  2. View calls viewModel.onSistolicaChanged() or viewModel.onDiastolicaChanged()
  3. ViewModel updates MedicionUiState
  4. View recomposes with new state
  5. User clicks “Guardar” button
  6. View calls viewModel.guardarMedicion()
  7. ViewModel validates input and calls repository.insertarMedicion()
  8. Repository calls medicionDao.insertar()
  9. Room persists data to SQLite database
  10. ViewModel emits success event via SharedFlow
  11. View collects event and shows Toast message

Dependency Injection

MiTensión uses manual dependency injection without a DI framework:
// In MedicionScreen.kt
val medicionDao = remember { AppDatabase.getDatabase(context).medicionDao() }
val repository = remember { MedicionRepository(medicionDao) }
val factory = remember { MedicionViewModelFactory(repository, errorViewModel) }
val viewModel: MedicionViewModel = viewModel(factory = factory)
Benefits:
  • No additional dependencies required
  • Simple and easy to understand
  • Full control over object creation
  • Good for small to medium projects
For larger projects, consider using Hilt or Koin for automated dependency injection.

State Management

UI State

MiTensión uses State<T> and MutableState<T> for UI state management:
private val _uiState = mutableStateOf(MedicionUiState())
val uiState: State<MedicionUiState> = _uiState
Pattern: Expose immutable state to View, keep mutable state private in ViewModel.

Events

One-time events (like showing Toast messages) use SharedFlow:
private val _evento = MutableSharedFlow<UiEvento>()
val evento = _evento.asSharedFlow()

sealed class UiEvento {
    data class MostrarMensaje(val mensaje: String) : UiEvento()
    data class GuardadoConExito(val mensaje: String) : UiEvento()
}

Database Observations

Room queries return Flow<T> for reactive data:
@Query("SELECT * FROM Medicion WHERE timestamp >= :inicioDelDia...")
fun obtenerMedicionesPorDia(inicioDelDia: Long, finDelDia: Long): Flow<List<Medicion>>

Threading Model

MiTensión uses Kotlin coroutines for asynchronous operations:

Main Thread

  • UI rendering
  • State updates
  • User interactions

Background Threads

  • Database operations (Room)
  • Suspend functions
  • Repository calls
fun guardarMedicion(...) {
    viewModelScope.launch { // Runs on Main dispatcher
        // Room automatically switches to background thread
        repository.insertarMedicion(nuevaMedicion)
        // Back to Main thread for UI updates
        _evento.emit(UiEvento.GuardadoConExito(mensajeExito))
    }
}
Room automatically handles threading - you don’t need to specify Dispatchers.IO for database operations.

Additional ViewModels

CalendarioViewModel

The Calendar screen uses a simpler state management approach for displaying monthly summaries. Location: app/src/main/java/com/fxn/mitension/ui/viewmodel/CalendarioUiState.kt State Definition:
data class CalendarioUiState(
    val fechaSeleccionada: LocalDate = LocalDate.now(),
    val resumenMensual: Map<Int, ResumenDiario> = emptyMap()
) {
    val anioMes: YearMonth = YearMonth.from(fechaSeleccionada)
}
Key Features:
  • fechaSeleccionada - Currently selected date for month navigation
  • resumenMensual - Map of day number to daily summary with period averages
  • anioMes - Computed property for year/month display
Navigation Functions:
fun mesSiguiente()  // Navigate to next month
fun mesAnterior()   // Navigate to previous month
Data Loading: The ViewModel uses flatMapLatest to automatically reload data when the month changes:
val uiState = fechaSeleccionadaFlow
    .flatMapLatest { fecha ->
        val inicio = fecha.atStartOfMonth()
        val fin = fecha.atEndOfMonth()
        repository.obtenerResumenMensual(inicio, fin)
            .map { lista ->
                CalendarioUiState(
                    fechaSeleccionada = fecha,
                    resumenMensual = lista.associateBy { it.dia }
                )
            }
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), CalendarioUiState())
The Map structure (Map<Int, ResumenDiario>) enables O(1) lookup when rendering calendar cells, significantly improving performance for 30-31 day grids.

DiaDetalleViewModel

The Day Detail screen displays all measurements for a specific day, grouped by period. Location: app/src/main/java/com/fxn/mitension/ui/viewmodel/DiaDetalleViewModel.kt State Definition:
data class DiaDetalleUiState(
    val medicionesAgrupadas: Map<PeriodoDelDia, List<Medicion>> = emptyMap(),
    val dia: Int = 0,
    val mes: Int = 0,
    val anio: Int = 0
)
Navigation Integration: The ViewModel receives date parameters from the navigation arguments using SavedStateHandle:
class DiaDetalleViewModel(
    savedStateHandle: SavedStateHandle,
    private val repository: MedicionRepository
) : ViewModel() {
    private val anio: Int = checkNotNull(savedStateHandle[AppDestinations.ANIO_ARG])
    private val mes: Int = checkNotNull(savedStateHandle[AppDestinations.MES_ARG])
    private val dia: Int = checkNotNull(savedStateHandle[AppDestinations.DIA_ARG])
}
Data Loading & Grouping:
  1. Calculates day timestamp range from date parameters
  2. Queries measurements for that day
  3. Groups by period using obtenerPeriodoParaTimestamp()
  4. Exposes as StateFlow for UI observation
val uiState: StateFlow<DiaDetalleUiState> = repository
    .obtenerMedicionesEnRango(inicioDia, finDia)
    .map { mediciones ->
        val agrupadas = mediciones.groupBy { 
            obtenerPeriodoParaTimestamp(it.timestamp) 
        }
        DiaDetalleUiState(
            medicionesAgrupadas = agrupadas,
            dia = dia,
            mes = mes,
            anio = anio
        )
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), DiaDetalleUiState())
Period Ordering: The UI displays periods in chronological order (Morning → Afternoon → Night) using the enum’s natural ordering.

Testing Strategy

Unit Testing ViewModels

class MedicionViewModelTest {
    @Test
    fun `saving valid measurement updates state correctly`() {
        val mockRepository = mockk<MedicionRepository>()
        val viewModel = MedicionViewModel(mockRepository)
        
        viewModel.onSistolicaChanged("120")
        viewModel.onDiastolicaChanged("80")
        viewModel.guardarMedicion(...)
        
        coVerify { mockRepository.insertarMedicion(any()) }
    }
}

Repository Testing

class MedicionRepositoryTest {
    @Test
    fun `repository delegates to DAO correctly`() = runTest {
        val mockDao = mockk<MedicionDao>()
        val repository = MedicionRepository(mockDao)
        
        repository.insertarMedicion(medicion)
        
        coVerify { mockDao.insertar(medicion) }
    }
}

Best Practices

ViewModel

  • Keep business logic in ViewModel
  • Use viewModelScope for coroutines
  • Expose immutable state to View
  • Never pass Context to ViewModel

Repository

  • Single source of truth
  • Abstract data sources
  • Use suspend functions for one-shot operations
  • Return Flow for observable data

View/Compose

  • Keep UI components stateless when possible
  • Hoist state to appropriate level
  • Use remember for expensive operations
  • Collect flows safely

Data Layer

  • Use Room for local persistence
  • Define clear entity relationships
  • Write efficient queries
  • Use DAOs for database access

Data Model

Explore the Room database schema and entities

UI Components

Learn about Jetpack Compose components

Build docs developers (and LLMs) love