Hola amigos 👋 Bienvenidos a un nuevo tutorial de Universo Android. Hoy aprenderemos sobre State Management en Jetpack Compose, el concepto fundamental para crear interfaces reactivas que responden automáticamente a cambios de datos.
Al finalizar este tutorial dominarás:
- State y MutableState
- remember y rememberSaveable
- derivedStateOf
- State Hoisting
- ViewModel con StateFlow
- Ejemplos prácticos completos
🟩 1. ¿Qué es State en Compose?
State es cualquier valor que puede cambiar con el tiempo. Cuando el estado cambia, Compose automáticamente recompone (redibuja) las partes de la UI que dependen de ese estado.
🟩 2. Conceptos Clave
- State: Valor observable
- MutableState: Estado modificable
- remember: Preserva estado durante recomposiciones
- rememberSaveable: Persiste estado en cambios de configuración
- State Hoisting: Elevar el estado a composables padres
🟩 3. Crear el Proyecto
Creamos un nuevo proyecto en Android Studio con Empty Activity y seleccionamos Compose.
🟩 4. Dependencias
📄 build.gradle (Module: app)
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
}🟩 5. State Básico con remember
📄 StateBasicsScreen.kt
package com.example.statemanagement
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun StateBasicsScreen() {
// Estado simple con remember
var counter by remember { mutableStateOf(0) }
var text by remember { mutableStateOf("") }
var isChecked by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "State Básico",
style = MaterialTheme.typography.headlineMedium,
color = Color(0xFF2196F3)
)
// Contador
CounterSection(
counter = counter,
onIncrement = { counter++ },
onDecrement = { counter-- },
onReset = { counter = 0 }
)
Divider()
// TextField con estado
TextFieldSection(
text = text,
onTextChange = { text = it }
)
Divider()
// Checkbox con estado
CheckboxSection(
isChecked = isChecked,
onCheckedChange = { isChecked = it }
)
}
}
@Composable
fun CounterSection(
counter: Int,
onIncrement: () -> Unit,
onDecrement: () -> Unit,
onReset: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF4CAF50)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Contador",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
Text(
"$counter",
style = MaterialTheme.typography.displayLarge,
color = Color.White
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = onDecrement) { Text("-") }
Button(onClick = onReset) { Text("Reset") }
Button(onClick = onIncrement) { Text("+") }
}
}
}
}
@Composable
fun TextFieldSection(
text: String,
onTextChange: (String) -> Unit
) {
Column {
OutlinedTextField(
value = text,
onValueChange = onTextChange,
label = { Text("Escribe algo") },
modifier = Modifier.fillMaxWidth()
)
Text(
"Caracteres: ${text.length}",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray,
modifier = Modifier.padding(top = 8.dp)
)
if (text.isNotEmpty()) {
Text(
"Texto en mayúsculas: ${text.uppercase()}",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF2196F3),
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
@Composable
fun CheckboxSection(
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = isChecked,
onCheckedChange = onCheckedChange
)
Text(
text = if (isChecked) "Activado ✓" else "Desactivado",
modifier = Modifier.padding(start = 8.dp)
)
}
}🟩 6. remember vs rememberSaveable
📄 RememberComparisonScreen.kt
package com.example.statemanagement
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun RememberComparisonScreen() {
// remember: se pierde en cambio de configuración
var rememberCounter by remember { mutableStateOf(0) }
// rememberSaveable: sobrevive cambio de configuración
var saveableCounter by rememberSaveable { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "remember vs rememberSaveable",
style = MaterialTheme.typography.headlineMedium,
color = Color(0xFF2196F3)
)
Text(
"Rota el dispositivo y observa la diferencia",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
// remember
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFF9800)
)
) {
Column(
modifier = Modifier.padding(24.dp)
) {
Text(
"remember",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
Text(
"Se pierde al rotar: $rememberCounter",
style = MaterialTheme.typography.bodyLarge,
color = Color.White
)
Button(
onClick = { rememberCounter++ },
modifier = Modifier.padding(top = 8.dp)
) {
Text("Incrementar")
}
}
}
// rememberSaveable
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF4CAF50)
)
) {
Column(
modifier = Modifier.padding(24.dp)
) {
Text(
"rememberSaveable",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
Text(
"Persiste al rotar: $saveableCounter",
style = MaterialTheme.typography.bodyLarge,
color = Color.White
)
Button(
onClick = { saveableCounter++ },
modifier = Modifier.padding(top = 8.dp)
) {
Text("Incrementar")
}
}
}
}
}🟩 7. derivedStateOf
📄 DerivedStateScreen.kt
package com.example.statemanagement
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun DerivedStateScreen() {
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
// derivedStateOf: calcula estado derivado eficientemente
val fullName by remember {
derivedStateOf { "$firstName $lastName".trim() }
}
val isValidName by remember {
derivedStateOf {
firstName.isNotBlank() && lastName.isNotBlank()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "derivedStateOf",
style = MaterialTheme.typography.headlineMedium,
color = Color(0xFF2196F3)
)
OutlinedTextField(
value = firstName,
onValueChange = { firstName = it },
label = { Text("Nombre") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = lastName,
onValueChange = { lastName = it },
label = { Text("Apellido") },
modifier = Modifier.fillMaxWidth()
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (isValidName) Color(0xFF4CAF50) else Color(0xFFFF9800)
)
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
"Nombre completo:",
style = MaterialTheme.typography.titleMedium,
color = Color.White
)
Text(
fullName.ifEmpty { "Ingresa tu nombre" },
style = MaterialTheme.typography.headlineSmall,
color = Color.White
)
Text(
"Estado: ${if (isValidName) "Válido ✓" else "Incompleto"}",
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
}🟩 8. State Hoisting
📄 StateHoistingScreen.kt
package com.example.statemanagement
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun StateHoistingScreen() {
// Estado elevado al padre
var quantity by remember { mutableStateOf(1) }
var price by remember { mutableStateOf(100.0) }
val total by remember {
derivedStateOf { quantity * price }
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "State Hoisting",
style = MaterialTheme.typography.headlineMedium,
color = Color(0xFF2196F3)
)
// Componente hijo sin estado (stateless)
QuantitySelector(
quantity = quantity,
onQuantityChange = { quantity = it }
)
PriceSelector(
price = price,
onPriceChange = { price = it }
)
TotalDisplay(total = total)
}
}
@Composable
fun QuantitySelector(
quantity: Int,
onQuantityChange: (Int) -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Cantidad:", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { if (quantity > 1) onQuantityChange(quantity - 1) },
enabled = quantity > 1
) {
Text("-")
}
Text(
"$quantity",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(horizontal = 16.dp)
)
Button(onClick = { onQuantityChange(quantity + 1) }) {
Text("+")
}
}
}
}
}
@Composable
fun PriceSelector(
price: Double,
onPriceChange: (Double) -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Precio unitario:", style = MaterialTheme.typography.titleMedium)
Slider(
value = price.toFloat(),
onValueChange = { onPriceChange(it.toDouble()) },
valueRange = 50f..500f,
modifier = Modifier.fillMaxWidth()
)
Text(
"$${"%.2f".format(price)}",
style = MaterialTheme.typography.headlineSmall,
color = Color(0xFF2196F3)
)
}
}
}
@Composable
fun TotalDisplay(total: Double) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF4CAF50)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Text(
"Total:",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
Text(
"$${"%.2f".format(total)}",
style = MaterialTheme.typography.displayMedium,
color = Color.White
)
}
}
}🟩 9. ViewModel con StateFlow
📄 CounterViewModel.kt
package com.example.statemanagement
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
data class CounterUiState(
val counter: Int = 0,
val history: List<String> = emptyList()
)
class CounterViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CounterUiState())
val uiState: StateFlow<CounterUiState> = _uiState.asStateFlow()
fun increment() {
_uiState.update { currentState ->
currentState.copy(
counter = currentState.counter + 1,
history = currentState.history + "Incrementado a ${currentState.counter + 1}"
)
}
}
fun decrement() {
_uiState.update { currentState ->
currentState.copy(
counter = currentState.counter - 1,
history = currentState.history + "Decrementado a ${currentState.counter - 1}"
)
}
}
fun reset() {
_uiState.update { currentState ->
currentState.copy(
counter = 0,
history = currentState.history + "Reseteado a 0"
)
}
}
fun clearHistory() {
_uiState.update { it.copy(history = emptyList()) }
}
}📄 ViewModelScreen.kt
package com.example.statemanagement
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ViewModelScreen(
viewModel: CounterViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "ViewModel + StateFlow",
style = MaterialTheme.typography.headlineMedium,
color = Color(0xFF2196F3)
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF9C27B0)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Text(
"Contador",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
Text(
"${uiState.counter}",
style = MaterialTheme.typography.displayLarge,
color = Color.White
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { viewModel.decrement() }) { Text("-") }
Button(onClick = { viewModel.reset() }) { Text("Reset") }
Button(onClick = { viewModel.increment() }) { Text("+") }
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
"Historial (${uiState.history.size})",
style = MaterialTheme.typography.titleMedium
)
if (uiState.history.isNotEmpty()) {
TextButton(onClick = { viewModel.clearHistory() }) {
Text("Limpiar")
}
}
}
Card(modifier = Modifier.fillMaxWidth()) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(uiState.history.reversed()) { item ->
Text(item, style = MaterialTheme.typography.bodyMedium)
}
}
}
}
}🟩 10. MainActivity
📄 MainActivity.kt
package com.example.statemanagement
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.example.statemanagement.ui.theme.StateManagementTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StateManagementTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
StateManagementApp()
}
}
}
}
}
@Composable
fun StateManagementApp() {
var selectedTab by remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxSize()) {
TabRow(selectedTabIndex = selectedTab) {
Tab(
selected = selectedTab == 0,
onClick = { selectedTab = 0 },
text = { Text("Básico") }
)
Tab(
selected = selectedTab == 1,
onClick = { selectedTab = 1 },
text = { Text("Remember") }
)
Tab(
selected = selectedTab == 2,
onClick = { selectedTab = 2 },
text = { Text("Derived") }
)
Tab(
selected = selectedTab == 3,
onClick = { selectedTab = 3 },
text = { Text("Hoisting") }
)
Tab(
selected = selectedTab == 4,
onClick = { selectedTab = 4 },
text = { Text("ViewModel") }
)
}
when (selectedTab) {
0 -> StateBasicsScreen()
1 -> RememberComparisonScreen()
2 -> DerivedStateScreen()
3 -> StateHoistingScreen()
4 -> ViewModelScreen()
}
}
}🟩 11. Reglas del State
| Regla | Descripción |
|---|---|
| Unidireccional | Los datos fluyen de arriba hacia abajo |
| Inmutable | No modifiques el estado directamente |
| Elevado | Eleva el estado al ancestro común |
| Observable | Usa State para recomposición automática |
▶️ Cómo Ejecutar
- Abre Android Studio
- Crea proyecto Compose
- Copia el código
- Presiona Run
- Explora las diferentes tabs
🧪 Resultado Final
Aplicación completa que demuestra todos los conceptos de State Management en Jetpack Compose.
📥 Descargar Proyecto
👉
🙌 Gracias por Visitar mi Blog
✔️ Compártelo
✔️ Déjame un comentario
✔️ SÃgueme para más contenido
❓ Preguntas Frecuentes
1. ¿Qué diferencia hay entre remember y rememberSaveable?
remember pierde el estado al rotar. rememberSaveable lo persiste en cambios de configuración.
2. ¿Cuándo usar derivedStateOf?
Para calcular estados derivados que solo deben recomponerse cuando sus dependencias cambien.
3. ¿Qué es State Hoisting?
Elevar el estado al composable padre para que los hijos sean stateless y reutilizables.
4. ¿Debo usar siempre ViewModel?
No siempre. Para estado simple usa remember. Para lógica compleja y persistencia usa ViewModel.

No hay comentarios:
Publicar un comentario