Blog para desarrollo de aplicaciones en Android, aprende paso a paso como crear aplicaciones.

State Management en Jetpack Compose: State, MutableState y remember

State Management en Jetpack Compose: State, MutableState y remember

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

ReglaDescripción
UnidireccionalLos datos fluyen de arriba hacia abajo
InmutableNo modifiques el estado directamente
ElevadoEleva el estado al ancestro común
ObservableUsa State para recomposición automática

▶️ Cómo Ejecutar

  1. Abre Android Studio
  2. Crea proyecto Compose
  3. Copia el código
  4. Presiona Run
  5. 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