Page cover image

ViewModel

El ViewModel es un componente esencial en la arquitectura de Android, particularmente en Jetpack Compose, ya que permite separar la lógica de negocio y los datos del ciclo de vida de la interfaz gráfica.

Los ViewModel están diseñados para:

  • Almacenar y gestionar datos de estado relacionados con la UI.

  • Sobrevivir a cambios de configuración como rotación de pantalla.

  • Facilitar la conexión entre la UI y la lógica de negocio.

En Jetpack Compose, un ViewModel permite emitir estados observables hacia las composables de forma reactiva.

Configuración

Incluye la siguiente dependencia en tu archivo build.gradle:

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")

Arquitectura CLEAN

Si seguimos la arquitetura CLEAN, los ViewModel deben declararse en el paquete presentation.viewmodel

com.example.myapp

├── data
│   ├── model
│   ├── repository
│   ├── source
│   │   ├── local
│   │   └── remote
│   └── mapper

├── domain
│   ├── model
│   ├── repository
│   └── usecase

├── presentation
│   ├── ui
│   │   ├── components
│   │   ├── screens
│   │   │   ├── home
│   │   │   ├── profile
│   │   │   └── settings
│   ├── navigation
│   └── viewmodel

├── di

└── utils

Crear un ViewModel

Un ViewModel se utiliza para manejar algún estado. Todo ViewModel debe declarar las siguientes partes:

  1. Variable de estado mutable (MutableStateFlow): define el estado que gestiona el ViewModel. Este estado es mutable y debe declararse privado para que los @Composable no puedan acceder al mismo directamente.

  2. Variable de estado inmutable (StateFlow): Es una derivación inmutable del estado anterior, este es público ya que

  3. Funciones de acciones del estado:

Ejemplo

Por ejemplo, podemos crear una aplicación para gestionar tareas (Task). Esta aplicación tendrá dos ventanas:

  • TasksScreen: Muestra un listado de tareas

  • CreateTaskScreen: Sirve para crear una nueva tarea.

Cada tarea la reprsentaremos con la data class Task

com.example.myApp.domain.model.Task.kt
data class Task(val id: Int, val title: String, val description: String)

TasksScreen

Esta ventana muestra una lista de tareas usando un LazyColumn. Para gestionar la lista de tareas crearemos TasksViewModel

com.example.myapp.presentation.viewmodel.tasks.TasksViewModel.kt
import androidx.lifecycle.ViewModel
import com.example.myandroidapp.domain.model.Task
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class TasksViewModel : ViewModel() {
    // Estado mutable que almacena el listado de tareas
    private val _tasks = MutableStateFlow<List<Task>>(
        listOf(
            Task(1, "Tarea 1", "Esta tarea hace cosas"),
            Task(2, "Tarea 2", "Esta tarea hace otras cosas"),
            Task(3, "Tarea 3", "Esta tarea hace no hace nada"),
            Task(4, "Tarea 4", "Bla bla bla"),
            Task(5, "Tarea 5", "La ultima tarea")
        )
    )
    // Version inmutable del estado anterior, es el estado que va a leer el UI
    val tasks: StateFlow<List<Task>> = _tasks

    // Elimina una tarea
    fun removeTask(id: Int) {
        // Quita la tarea con el id del parametro de la lista
        _tasks.value = _tasks.value.filter { it.id != id }
    }
}

Después usamos el ViewModel en la ventana TasksScreen

com.example.myapp.presentation.ui.screens.tasks.TaskScreen.kt
import TasksViewModel
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.myandroidapp.domain.model.Task
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.runtime.getValue

@Composable
fun TaskScreen(tasksViewModel: TasksViewModel = viewModel()) {
    val tasks by tasksViewModel.tasks.collectAsState()
    
    Scaffold { innerPadding ->
        Column(Modifier.padding(innerPadding)) {
            Text("Listado de tareas")
            Spacer(modifier = Modifier.height(16.dp))
            LazyColumn {
                items(tasks) { task ->
                    key(task) {
                        TaskCard(task = task, tasksViewModel = tasksViewModel)
                    }
                }
            }
        }
    }
}

@Composable
fun TaskCard(task: Task, tasksViewModel: TasksViewModel) {
    Card(Modifier.fillMaxWidth()) {
        Column {
            Text(task.title)
            
            Spacer(modifier = Modifier.height(16.dp))
            
            Text(task.description)
            
            Spacer(modifier = Modifier.height(16.dp))

            IconButton(onClick = { tasksViewModel.removeTask(task.id) }) {
                Icon(
                    imageVector = Icons.Default.Remove,
                    contentDescription = "Icono de eliminar"
                )
            }
        }
    }
}

CreateTaskScreen

Primer creamos el ViewModel que gestiona los datos de una tarea

import androidx.lifecycle.ViewModel
import com.example.tasksapp.domain.model.Task
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class CreateTaskViewModel : ViewModel() {
    // Estado mutable que almacena el listado de tareas
    private val _task = MutableStateFlow<Task>(
        Task(0, "", "")
    )
    // Version inmutable del estado anterior, es el estado que va a leer el UI
    val task: StateFlow<Task> = _task
    
    
    fun setTitle(title: String) {
        _task.value = _task.value.copy(title = title)
    }
    
    fun setDescription(description: String) {
        _task.value = _task.value.copy(description = description)
    }
    
    
    fun save() {
        // TODO Aquí va la lógica para guardar la tarea
        // Veremos qué se hace en el próximo tema
    }
}

Ahora ya podemos usarlo en la ventana CreateTaskScreen

package com.example.tasksapp.presentation.ui.screens.tasks

import CreateTaskViewModel
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.runtime.getValue


@Composable
fun CreateTaskScreen(createTaskViewModel: CreateTaskViewModel = viewModel()) {
    val task by createTaskViewModel.task.collectAsState()

    Scaffold(
        topBar = {
            Text("Crear tarea")
        }
    ) { innerPadding ->
        Column(Modifier.padding(innerPadding)) {
            TextField(
                modifier = Modifier.fillMaxWidth(),
                value = task.title,
                onValueChange = {
                    createTaskViewModel.setTitle(it)
                }
            )
            Spacer(modifier = Modifier.height(16.dp))

            TextField(
                modifier = Modifier.fillMaxWidth(),
                value = task.description,
                onValueChange = {
                    createTaskViewModel.setDescription(it)
                }
            )
            Spacer(modifier = Modifier.height(16.dp))

            Button(
                modifier = Modifier.align(Alignment.CenterHorizontally),
                onClick = { createTaskViewModel.save() }
            ) {
                Text("Guardar")
            }
        }
    }
}

Última actualización

¿Te fue útil?