Universidad del Quindío
Programa de Ingeniería de Sistemas y Computación
Título: Arquitectura MVVM y Repositorios en Android
Docente: Carlos Andrés Florez V.

Arquitectura MVVM y Repositorios en Android

Introducción

En las clases anteriores, hemos explorado los conceptos fundamentales de la arquitectura MVVM (Model-View-ViewModel) y cómo implementarla en aplicaciones Android utilizando Jetpack Compose. En esta guía, profundizaremos en el uso de repositorios para gestionar la lógica de datos y mejorar la separación de responsabilidades dentro de nuestra aplicación.

La idea es crear una aplicación más escalable y mantenible, donde los ViewModels se encarguen únicamente de preparar los datos para la UI, mientras que los repositorios manejen la lógica de acceso a datos, ya sea desde una base de datos local, una API remota o cualquier otra fuente de datos.

Por este motivo, utilizaremos una arquitectura basada en capas:

Una arquitectura bien definida no solo mejora la organización del código, sino que también facilita las pruebas unitarias y la evolución de la aplicación a lo largo del tiempo.

Conceptos Clave

Antes de comenzar con la implementación, es importante entender algunos conceptos clave:

ViewModel

Los ViewModels son componentes clave en la arquitectura MVVM. Actúan como intermediarios entre la vista (UI) y el modelo (datos). Su principal responsabilidad es preparar y gestionar los datos para la UI, asegurando que la lógica de negocio esté separada de la presentación. Ya hemos trabajado con ViewModels en las clases anteriores, pero ahora los integraremos con repositorios para una mejor gestión de datos.

Repositorios

Los repositorios son una capa adicional que se sitúa entre el ViewModel y las fuentes de datos (como bases de datos locales, servicios web, etc.). Su función principal es abstraer la lógica de acceso a datos, proporcionando una interfaz limpia para que el ViewModel pueda interactuar con los datos sin preocuparse por los detalles de implementación.

Se dividen en dos partes:

Casos de Uso (Use Cases)

Un caso de uso representa una única acción de negocio de la aplicación: iniciar sesión, registrar un usuario, observar la lista de reportes, crear un reporte, etc. Cada caso de uso es una clase pequeña con una sola responsabilidad, que se ubica en la capa de dominio (paquete domain/usecase).

Los casos de uso se sitúan entre el ViewModel y el repositorio. En lugar de que el ViewModel llame directamente al repositorio, el ViewModel invoca casos de uso, y son los casos de uso los que orquestan al repositorio (o a varios repositorios y otras fuentes de datos). Esto aporta varias ventajas:

Por convención, un caso de uso expone una única función con el operador invoke, lo que permite ejecutarlo como si fuera una función (por ejemplo loginUserUseCase(email, password) en lugar de loginUserUseCase.invoke(email, password)).

Hilt en Jetpack Compose

Hilt es una biblioteca de inyección de dependencias para Android que facilita la gestión de dependencias en aplicaciones. En el contexto de Jetpack Compose, Hilt permite inyectar ViewModels y repositorios de manera sencilla, promoviendo un código más limpio y modular.

Básicamente, Hilt se encarga de crear y proporcionar las instancias necesarias de los ViewModels y repositorios cuando se requieren, eliminando la necesidad de instanciarlos manualmente.

Para más información sobre Hilt, puede consultar la documentación oficial: Hilt Documentation.

KSP (Kotlin Symbol Processing)

KSP es una herramienta de procesamiento de símbolos para Kotlin que permite generar código a partir de anotaciones. A diferencia de Kapt, que se basa en la generación de código Java, KSP trabaja directamente con el código Kotlin, lo que puede resultar en un rendimiento mejorado y una integración más fluida con proyectos Kotlin.

KSP es compatible con bibliotecas como Hilt, permitiendo la inyección de dependencias en aplicaciones Android escritas en Kotlin. Al utilizar KSP, los desarrolladores pueden aprovechar las ventajas de la generación de código optimizada para Kotlin, facilitando la gestión de dependencias y facilitando la integración con otras bibliotecas y herramientas del ecosistema Kotlin.

Para más información sobre KSP, puede consultar la documentación oficial: KSP Documentation.

Diagrama de la arquitectura

Con el fin de visualizar mejor la arquitectura que vamos a implementar, aquí se muestra un diagrama que representa las diferentes capas y cómo interactúan entre sí:

Diagrama de la arquitectura MVVM con repositorios


Integración en el proyecto

Abra su proyecto de Android Studio donde ha estado trabajando en las clases anteriores y siga los siguientes pasos para integrar Hilt y configurar los repositorios.

1. Agregar dependencias

Agregue lo siguiente en el archivo libs.versions.toml para incluir Hilt y Ksp en su proyecto:

[versions]
hiltAndroid = "2.59.2"
hiltNavigationCompose = "1.3.0"
ksp = "2.3.6" # Desde KSP 2.3.0 la versión ya no va atada a la versión de Kotlin

[libraries]
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroid" }

[plugins]
hilt-android = { id="com.google.dagger.hilt.android", version.ref="hiltAndroid"}
devtools-ksp = { id="com.google.devtools.ksp", version.ref="ksp" }

Ahora, en el archivo build.gradle del proyecto, aplique el plugin de Hilt:

plugins {
    alias(libs.plugins.hilt.android) apply false
    alias(libs.plugins.devtools.ksp) apply false
}

Por último, en el archivo build.gradle del módulo de la aplicación, agregue las siguientes dependencias:

plugins {
    alias(libs.plugins.hilt.android)
    alias(libs.plugins.devtools.ksp)
}

dependencies {
    // --- Hilt Core ---
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)

    // --- Hilt + Compose Navigation ---
    implementation(libs.androidx.hilt.navigation.compose)
}

⚠️ Importante: Recuerde no borrar las dependencias que ya tiene en su proyecto. Solo agregue las nuevas. Asegúrese de sincronizar el proyecto después de agregar las dependencias para que se descarguen correctamente.

2. Configurar Hilt en la aplicación

Cree una clase que extienda de Application y anótela con @HiltAndroidApp. Esto inicializa Hilt en su aplicación. Por ejemplo, cree un archivo llamado MyApp.kt con el siguiente contenido:

package com.example.demoapp

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApp : Application()

3. Configurar el AndroidManifest.xml

En el archivo AndroidManifest.xml, asegúrese de declarar la clase de aplicación que acaba de crear:

<application
    android:name=".MyApp"
    ... >
    ...
</application>

De esta manera, Android sabrá que debe usar esta clase como la aplicación principal y Hilt podrá inicializarse correctamente.

4. Configurar la actividad principal

En su actividad principal (por ejemplo, MainActivity), agregue la anotación @AndroidEntryPoint para habilitar la inyección de dependencias en esta clase.

5. Crear las interfaces de los Repositorios

Comencemos por el repositorio de reportes, que es el eje central de la aplicación. En el paquete domain/repository, cree una nueva interfaz llamada ReportRepository con el siguiente contenido:

package com.example.demoapp.domain.repository

import com.example.demoapp.domain.model.Report
import kotlinx.coroutines.flow.StateFlow

interface ReportRepository {
    val reports: StateFlow<List<Report>>
    suspend fun save(report: Report)
    suspend fun findById(id: String): Report?
}

Adicionalmente, necesitamos un repositorio para los usuarios, que por ahora usaremos para el registro y el inicio de sesión. En el mismo paquete, cree la interfaz UserRepository:

package com.example.demoapp.domain.repository

import com.example.demoapp.domain.model.User

interface UserRepository {
    suspend fun save(user: User)
    suspend fun findById(id: String): User?
    suspend fun login(email: String, password: String): User?
}

La idea es definir las operaciones que cada repositorio debe implementar para gestionar sus datos, pero sin preocuparse por los detalles de implementación.

⚠️ Importante: Declaramos las operaciones de acceso a datos como suspend desde el inicio. Aunque nuestra primera implementación trabajará con datos en memoria (donde no sería estrictamente necesario), en guías posteriores conectaremos los repositorios a fuentes asíncronas (Room, APIs REST, Firestore). Definir el contrato como suspend ahora evita tener que cambiar las interfaces más adelante. La propiedad reports no es suspend porque es un StateFlow que se observa de forma reactiva.

6. Crear los Repositorios para gestionar los datos

Organizaremos las implementaciones de los repositorios en subpaquetes según su fuente de datos: memory (en memoria), local (base de datos) y remote (servicios externos). Como estas primeras implementaciones trabajan con datos en memoria, cree las clases en el paquete data/repository/memory. Más adelante, al conectar Room o Firestore, añadiremos implementaciones en local y remote sin tocar el resto de la app.

En el paquete data/repository/memory, cree una clase llamada ReportRepositoryImpl que implemente la interfaz ReportRepository. Esta clase manejará la lógica de datos de los reportes; los datos de ejemplo que teníamos en el ReportListViewModel ahora se centralizan aquí:

package com.example.demoapp.data.repository.memory

import com.example.demoapp.domain.model.Location
import com.example.demoapp.domain.model.Report
import com.example.demoapp.domain.model.ReportStatus
import com.example.demoapp.domain.repository.ReportRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.time.LocalDate
import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton // Anotamos la clase como Singleton para que Hilt gestione una única instancia
class ReportRepositoryImpl @Inject constructor(): ReportRepository { // Implementamos la interfaz ReportRepository

    // Usamos StateFlow para manejar la lista de reportes de manera reactiva
    private val _reports = MutableStateFlow<List<Report>>(emptyList())
    override val reports: StateFlow<List<Report>> = _reports.asStateFlow()

    init {
        _reports.value = fetchReports()
    }

    override suspend fun save(report: Report) {
        _reports.value += report
    }

    override suspend fun findById(id: String): Report? {
        return _reports.value.firstOrNull { it.id == id }
    }

    private fun fetchReports(): List<Report> {
        return listOf(
            Report(
                id = "1",
                title = "Hueco en la vía",
                description = "Hueco grande en plena calzada, los vehículos deben esquivarlo",
                location = Location(latitude = 4.5339, longitude = -75.6811),
                status = ReportStatus.PENDING,
                type = "Infraestructura",
                photoUrl = "https://picsum.photos/200?random=1",
                ownerId = "1",
                date = LocalDate.now()
            ),
            Report(
                id = "2",
                title = "Alumbrado dañado",
                description = "Poste de luz sin funcionar desde hace una semana",
                location = Location(latitude = 4.5402, longitude = -75.6658),
                status = ReportStatus.VERIFIED,
                type = "Alumbrado",
                photoUrl = "https://picsum.photos/200?random=2",
                ownerId = "2",
                date = LocalDate.now().minusDays(3)
            ),
            Report(
                id = "3",
                title = "Basura acumulada",
                description = "Acumulación de basura en la esquina del parque",
                location = Location(latitude = 4.5475, longitude = -75.6585),
                status = ReportStatus.RESOLVED,
                type = "Basura",
                photoUrl = "https://picsum.photos/200?random=3",
                ownerId = "1",
                date = LocalDate.now().minusWeeks(1)
            )
        )
    }
}

Ahora, en el mismo paquete, cree la clase UserRepositoryImpl que implementa la interfaz UserRepository, con algunos usuarios de ejemplo para poder probar el inicio de sesión:

package com.example.demoapp.data.repository.memory

import com.example.demoapp.domain.model.User
import com.example.demoapp.domain.model.UserRole
import com.example.demoapp.domain.repository.UserRepository
import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
class UserRepositoryImpl @Inject constructor(): UserRepository {

    // Lista de usuarios en memoria con algunos datos de ejemplo
    private val users = mutableListOf(
        User(
            id = "1",
            name = "Juan",
            city = "Ciudad 1",
            address = "Calle 123",
            email = "juan@email.com",
            password = "111111",
            profilePictureUrl = "https://m.media-amazon.com/images/I/41g6jROgo0L.png"
        ),
        User(
            id = "2",
            name = "Maria",
            city = "Pereira",
            address = "Calle 456",
            email = "maria@email.com",
            password = "222222",
            profilePictureUrl = "https://picsum.photos/200?random=2"
        ),
        User(
            id = "3",
            name = "Carlos",
            city = "Armenia",
            address = "Calle 789",
            email = "carlos@email.com",
            password = "333333",
            profilePictureUrl = "https://picsum.photos/200?random=3",
            role = UserRole.ADMIN
        )
    )

    override suspend fun save(user: User) {
        users.add(user)
    }

    override suspend fun findById(id: String): User? {
        return users.firstOrNull { it.id == id }
    }

    override suspend fun login(email: String, password: String): User? {
        return users.firstOrNull { it.email == email && it.password == password }
    }
}

La idea es que cada repositorio maneje toda la lógica relacionada con sus datos: el de reportes permite observarlos, crearlos y buscarlos por ID, mientras que el de usuarios permite guardar nuevos usuarios, buscarlos por ID y manejar el inicio de sesión. Además, estas clases implementan un patrón Singleton para asegurar que solo exista una instancia de cada repositorio en toda la aplicación.

7. Crear los Casos de Uso

Antes de modificar los ViewModels, vamos a crear los casos de uso que encapsulan cada acción de negocio. Recuerde que los ViewModels no llamarán al repositorio directamente, sino a través de estos casos de uso.

Cada caso de uso es una clase con una única responsabilidad, ubicada en el paquete domain/usecase. Organizaremos los casos de uso en subpaquetes según el área a la que pertenecen (report, user, auth, etc.).

⚠️ Importante: Los casos de uso son clases concretas con un constructor anotado con @Inject, por lo que Hilt puede crearlos automáticamente sin necesidad de un módulo adicional.

En el paquete domain/usecase/report, cree el caso de uso para observar la lista de reportes. Cree un archivo llamado ObserveReportsUseCase.kt:

package com.example.demoapp.domain.usecase.report

import com.example.demoapp.domain.model.Report
import com.example.demoapp.domain.repository.ReportRepository
import kotlinx.coroutines.flow.StateFlow
import jakarta.inject.Inject

class ObserveReportsUseCase @Inject constructor(
    private val reportRepository: ReportRepository
) {
    // El operador invoke permite ejecutar el caso de uso como una función: observeReportsUseCase()
    operator fun invoke(): StateFlow<List<Report>> = reportRepository.reports
}

En el mismo paquete, cree el caso de uso para buscar un reporte por su ID. Cree un archivo llamado GetReportByIdUseCase.kt:

package com.example.demoapp.domain.usecase.report

import com.example.demoapp.domain.model.Report
import com.example.demoapp.domain.repository.ReportRepository
import jakarta.inject.Inject

class GetReportByIdUseCase @Inject constructor(
    private val reportRepository: ReportRepository
) {
    // Es suspend porque el repositorio expone findById como una operación suspend
    suspend operator fun invoke(reportId: String): Report? = reportRepository.findById(reportId)
}

También en domain/usecase/report, cree el caso de uso para crear un nuevo reporte. Cree un archivo llamado CreateReportUseCase.kt:

package com.example.demoapp.domain.usecase.report

import com.example.demoapp.domain.model.Report
import com.example.demoapp.domain.repository.ReportRepository
import jakarta.inject.Inject

class CreateReportUseCase @Inject constructor(
    private val reportRepository: ReportRepository
) {
    suspend operator fun invoke(report: Report) {
        reportRepository.save(report)
    }
}

Ahora pasemos a los casos de uso relacionados con los usuarios. En el paquete domain/usecase/user, cree el caso de uso para buscar un usuario por su ID (nos será útil más adelante, por ejemplo, para mostrar los datos del autor de un reporte a partir de su ownerId). Cree un archivo llamado GetUserByIdUseCase.kt:

package com.example.demoapp.domain.usecase.user

import com.example.demoapp.domain.model.User
import com.example.demoapp.domain.repository.UserRepository
import jakarta.inject.Inject

class GetUserByIdUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(userId: String): User? = userRepository.findById(userId)
}

En el mismo paquete, cree el caso de uso para registrar un usuario. Cree un archivo llamado RegisterUserUseCase.kt:

package com.example.demoapp.domain.usecase.user

import com.example.demoapp.domain.model.User
import com.example.demoapp.domain.repository.UserRepository
import jakarta.inject.Inject

class RegisterUserUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(user: User) {
        userRepository.save(user)
    }
}

Finalmente, en el paquete domain/usecase/auth, cree el caso de uso para iniciar sesión. Cree un archivo llamado LoginUserUseCase.kt:

package com.example.demoapp.domain.usecase.auth

import com.example.demoapp.domain.model.User
import com.example.demoapp.domain.repository.UserRepository
import jakarta.inject.Inject

class LoginUserUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(email: String, password: String): User? {
        return userRepository.login(email, password)
    }
}

Observe que, por ahora, cada caso de uso simplemente delega en el repositorio. Esto puede parecer un paso intermedio innecesario, pero más adelante (cuando trabajemos con sesiones, autenticación remota, etc.) los casos de uso coordinarán varias fuentes de datos, y es ahí donde su valor se hace evidente.

8. Modificar los ViewModels para usar los Casos de Uso

Modifique el ReportListViewModel para que utilice el ObserveReportsUseCase en lugar de manejar los datos de ejemplo directamente. Actualice el archivo ReportListViewModel.kt con el siguiente contenido:

package com.example.demoapp.features.report.list

import androidx.lifecycle.ViewModel
import com.example.demoapp.domain.model.Report
import com.example.demoapp.domain.usecase.report.ObserveReportsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import jakarta.inject.Inject

@HiltViewModel // Anotamos el ViewModel con @HiltViewModel para que Hilt pueda inyectarlo
class ReportListViewModel @Inject constructor(
    observeReportsUseCase: ObserveReportsUseCase
) : ViewModel() {

    // Exponemos la lista de reportes a través del caso de uso para que la UI pueda observar los cambios
    val reports: StateFlow<List<Report>> = observeReportsUseCase()

}

Note cómo el ViewModel quedó mucho más delgado: ya no contiene los datos de ejemplo ni las funciones findById y save, porque esa lógica ahora vive en el repositorio y se accede a través de los casos de uso.

Ajuste o cree el ReportDetailViewModel (en features/report/detail) para que utilice el GetReportByIdUseCase y obtenga los detalles de un reporte específico. Como el caso de uso es suspend, debemos invocarlo dentro de una corutina (viewModelScope.launch) y exponer el resultado mediante un StateFlow que la pantalla pueda observar. Actualice el archivo ReportDetailViewModel.kt con el siguiente contenido:

package com.example.demoapp.features.report.detail

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.demoapp.domain.model.Report
import com.example.demoapp.domain.usecase.report.GetReportByIdUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import jakarta.inject.Inject

@HiltViewModel
class ReportDetailViewModel @Inject constructor(
    private val getReportByIdUseCase: GetReportByIdUseCase
) : ViewModel() {

    // Estado para el reporte que la pantalla observará con collectAsState()
    private val _report = MutableStateFlow<Report?>(null)
    val report: StateFlow<Report?> = _report.asStateFlow()

    fun findById(reportId: String) {
        // El caso de uso es suspend, por eso lo llamamos dentro de viewModelScope
        viewModelScope.launch {
            _report.value = getReportByIdUseCase(reportId)
        }
    }
}

En la pantalla, observe report con collectAsState() y dispare la carga con un LaunchedEffect(reportId) { viewModel.findById(reportId) }.

Así mismo, modifique el LoginViewModel y el RegisterViewModel para que utilicen el LoginUserUseCase y el RegisterUserUseCase, respectivamente, en lugar de inyectar el repositorio. Recuerde que, al ser casos de uso suspend, deberá invocarlos dentro de viewModelScope.launch.

Dado que todos los ViewModels ahora dependen de casos de uso (y estos, a su vez, de ReportRepository o UserRepository), Hilt se encargará de inyectar automáticamente todas las instancias necesarias cuando se creen los ViewModels, de esta manera, la lógica de negocio queda encapsulada y los datos centralizados.

9. Configurar la inyección de dependencias para los Repositorios

Dado que ReportRepositoryImpl y UserRepositoryImpl son las implementaciones concretas de las interfaces ReportRepository y UserRepository, necesitamos indicarle a Hilt cómo proporcionar estas implementaciones cuando se soliciten.

Cree un módulo de Hilt para definir estas vinculaciones. Cree un nuevo paquete llamado di (inyección de dependencias) y dentro de este paquete, cree un archivo llamado RepositoryModule.kt con el siguiente contenido:

package com.example.demoapp.di

import com.example.demoapp.data.repository.memory.ReportRepositoryImpl
import com.example.demoapp.data.repository.memory.UserRepositoryImpl
import com.example.demoapp.domain.repository.ReportRepository
import com.example.demoapp.domain.repository.UserRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import jakarta.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds // Indica a Hilt que esta función vincula una implementación a una interfaz
    @Singleton
    abstract fun bindReportRepository(
        reportRepositoryImpl: ReportRepositoryImpl
    ): ReportRepository // Vincula ReportRepositoryImpl con ReportRepository

    @Binds
    @Singleton
    abstract fun bindUserRepository(
        userRepositoryImpl: UserRepositoryImpl
    ): UserRepository // Vincula UserRepositoryImpl con UserRepository
}

Este módulo es clave, ya que si no se crea, Hilt no sabrá cómo proporcionar una instancia de ReportRepository o UserRepository cuando se inyecte en los casos de uso.

⚠️ Note que no creamos un módulo para los casos de uso. A diferencia de las interfaces de los repositorios (que necesitan que le indiquemos cuál implementación usar mediante @Binds), los casos de uso son clases concretas con constructor @Inject, así que Hilt sabe construirlos por sí mismo.

10. Inyectar el ViewModel en las pantallas

Finalmente, modifique las pantallas para inyectar los ViewModels utilizando Hilt. Por ejemplo, en la pantalla de lista de reportes (ReportListScreen), inyecte el ReportListViewModel de la siguiente manera:

package com.example.demoapp.features.report.list

@Composable
fun ReportListScreen(
    onNavigateToReportDetail: (String) -> Unit,
    onNavigateToCreateReport: () -> Unit,
    padding: PaddingValues = PaddingValues(),
    reportsViewModel: ReportListViewModel = hiltViewModel()
){
    // Resto del código...
} 

De esta manera, el ReportListViewModel se inyectará automáticamente en la pantalla utilizando Hilt, y podrá acceder a los datos gestionados por el ReportRepository.

Ajuste también la pantalla de detalles del reporte (ReportDetailScreen) para inyectar el ReportDetailViewModel de manera similar:

package com.example.demoapp.features.report.detail

@Composable
fun ReportDetailScreen(
    padding: PaddingValues,
    reportId: String,
    reportViewModel: ReportDetailViewModel = hiltViewModel(),
){
    // Resto del código...
}

Cambie las demás pantallas de manera similar para inyectar los ViewModels correspondientes usando hiltViewModel().

11. Pruebe la aplicación

Ejecute la aplicación para asegurarse de que todo funcione correctamente con la nueva arquitectura MVVM y el uso de casos de uso y repositorios. Verifique que las funcionalidades de registro, inicio de sesión, lista de reportes y detalle del reporte funcionen como se espera.

Por ahora, los datos se gestionan en memoria a través de los repositorios. En futuras guías, exploraremos cómo integrar fuentes de datos persistentes, como bases de datos locales o servicios web, para mejorar aún más la gestión de datos en la aplicación.


Actividad práctica

1. Pantalla de login

Ajuste la pantalla de login para que utilice el LoginViewModel con el UserRepository para autenticar a los usuarios. Asegúrese de que al iniciar sesión, se verifique la existencia del usuario en el repositorio y se asigne el estado de login correctamente (éxito o fallo).

Para que esto funcione, modifique el LoginViewModel para que inyecte el LoginUserUseCase y verifique las credenciales del usuario al iniciar sesión, de la siguiente manera:

fun login() {
    if (isFormValid) {
        // El caso de uso es suspend, por eso lo invocamos dentro de viewModelScope
        viewModelScope.launch {
            // Se invoca el caso de uso de login
            val user = loginUserUseCase(email.value, password.value)
            // Actualizamos el estado del resultado del login
            _loginResult.value = if (user != null) {
                RequestResult.Success("Login exitoso")
            } else {
                RequestResult.Failure("Credenciales inválidas")
            }
        }
    }
}

Dado que LoginScreen ya está configurada para observar el estado del login a través de un LaunchedEffect, al actualizar loginResult en el ViewModel, la pantalla reaccionará automáticamente a los cambios y navegará a la pantalla principal (la lista de reportes) si el login es exitoso.

Pruebe la pantalla de login para asegurarse de que el inicio de sesión funcione correctamente utilizando el repositorio con datos correctos e incorrectos.

2. Pantalla de registro

Ajuste la pantalla de registro para que utilice el RegisterViewModel con el RegisterUserUseCase para registrar nuevos usuarios. Puede modificar el método register en el RegisterViewModel de la siguiente manera:

fun register() {
    if (isFormValid) {
        // El caso de uso es suspend, por eso lo invocamos dentro de viewModelScope
        viewModelScope.launch {
            val newUser = User(
                id = UUID.randomUUID().toString(), // Genera un ID único para el nuevo usuario
                name = name.value,
                city = city.value,
                address = address.value,
                email = email.value,
                password = password.value,
                profilePictureUrl = profilePictureUrl.value
            )
            registerUserUseCase(newUser)
            _registerResult.value = RequestResult.Success("Registro exitoso")
        }
    }
}

Asegúrese de que al registrar un nuevo usuario, se guarde en el repositorio y se navegue a la pantalla de login (solo si el registro es exitoso). Use el mismo enfoque que en la pantalla de login, utilizando un LaunchedEffect para observar el resultado del registro y navegar a la pantalla de login si el registro es exitoso.

3. Pantalla de creación de reportes

Ajuste la pantalla CreateReportScreen para que utilice un CreateReportViewModel (inyectado con Hilt) que invoque el CreateReportUseCase, en lugar de agregar el reporte directamente en el ViewModel de la lista. Puede implementar la función create en el CreateReportViewModel de la siguiente manera:

fun create() {
    if (isFormValid) {
        // El caso de uso es suspend, por eso lo invocamos dentro de viewModelScope
        viewModelScope.launch {
            val newReport = Report(
                id = UUID.randomUUID().toString(), // Genera un ID único para el nuevo reporte
                title = title.value,
                description = description.value,
                type = type.value,
                location = Location(latitude = 0.0, longitude = 0.0), // Por ahora una ubicación fija; la capturaremos con el mapa más adelante
                status = ReportStatus.PENDING, // Todo reporte nace en estado pendiente
                photoUrl = "https://picsum.photos/200", // Por ahora una imagen fija; más adelante la tomaremos con la cámara o galería
                ownerId = "1", // Por ahora un ID fijo; más adelante saldrá de la sesión del usuario autenticado
                date = LocalDate.now() // Fecha de creación del reporte
            )
            createReportUseCase(newReport)
            _createResult.value = RequestResult.Success("Reporte creado exitosamente")
        }
    }
}

Use el mismo enfoque que en las pantallas de login y registro: un LaunchedEffect que observe el resultado de la creación y, si es exitoso, regrese a la lista de reportes (donde el nuevo reporte debe aparecer automáticamente, gracias a que la lista observa el StateFlow del repositorio).