Desarrollo móvil Intermedio

¡Hola y bienvenido a "Tu código cotidiano"! En este tutorial, te guiaré en la construcción de "Helium", un fascinante concepto de aplicación Android que he desarrollado desde cero con Jetpack Compose. La …

Publicado: 27/07/2025 Por: Juan Felipe Orozco Cortés Duración: 45 - 60 minutos. Nivel: Intermedio
Contenido del tutorial

¡Hola y bienvenido a "Tu código cotidiano"! En este tutorial, te guiaré en la construcción de "Helium", un fascinante concepto de aplicación Android que he desarrollado desde cero con Jetpack Compose. La idea central es crear una red local de pares (P2P) para ahorrar datos móviles compartiendo actualizaciones de aplicaciones directamente con dispositivos cercanos. Juntos construiremos paso a paso toda la arquitectura de la interfaz de usuario (UI): desde la configuración del proyecto, un sistema de navegación seguro y un tema visual robusto, hasta el diseño de cada pantalla con componentes reutilizables, gráficos personalizados y animaciones. Para mantener el enfoque, el tutorial se centra exclusivamente en el desarrollo de la interfaz, dejando la lógica de negocio fundamental (como la conexión P2P real o la transferencia de archivos) para futuras exploraciones. Nuestro objetivo es dominar la creación de una UI moderna y reactiva y ver cómo una idea cobra vida a través del código.

MainActivity.kt

A continuación, se presenta el código fuente del archivo MainActivity.kt. Esta actividad es el punto de entrada principal de la aplicación Android "Helium". Utiliza Jetpack Compose para construir una interfaz de usuario moderna y reactiva. Su función principal es configurar la estructura de la aplicación, incluyendo un tema visual, una barra de navegación inferior y el sistema de enrutamiento para cambiar entre las diferentes pantallas (Home, Network, Activity y Settings).

Helium: Código de la Actividad Principal (MainActivity.kt)

// Reemplaza TODO el contenido de MainActivity.kt

package com.felipeorozco.helium

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.felipeorozco.helium.ui.navigation.AppScreens
import com.felipeorozco.helium.ui.navigation.bottomNavItems
import com.felipeorozco.helium.ui.screens.ActivityScreen
import com.felipeorozco.helium.ui.screens.HomeScreen
import com.felipeorozco.helium.ui.screens.NetworkScreen
import com.felipeorozco.helium.ui.screens.SettingsScreen
import com.felipeorozco.helium.ui.theme.HeliumTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            HeliumTheme {
                HeliumApp()
            }
        }
    }
}

@Composable
fun HeliumApp() {
    val navController = rememberNavController()

    Scaffold(
        bottomBar = {
            NavigationBar {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentDestination = navBackStackEntry?.destination

                bottomNavItems.forEach { screen ->
                    NavigationBarItem(
                        selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
                        onClick = {
                            navController.navigate(screen.route) {
                                // Pop up to the start destination of the graph to
                                // avoid building up a large stack of destinations
                                // on the back stack as users select items
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                // Avoid multiple copies of the same destination when
                                // reselecting the same item
                                launchSingleTop = true
                                // Restore state when reselecting a previously selected item
                                restoreState = true
                            }
                        },
                        icon = { Icon(imageVector = screen.icon, contentDescription = screen.title) },
                        label = { Text(text = screen.title) }
                    )
                }
            }
        }
    ) { innerPadding ->
        Box(modifier = Modifier.padding(innerPadding)) {
            NavHost(navController = navController, startDestination = AppScreens.Home.route) {
                composable(AppScreens.Home.route) { HomeScreen() }
                composable(AppScreens.Network.route) { NetworkScreen() }
                composable(AppScreens.Activity.route) { ActivityScreen() }
                composable(AppScreens.Settings.route) { SettingsScreen() }
            }
        }
    }
}

Explicación del código: Helium: Código de la Actividad Principal (MainActivity.kt)

1. package com.felipeorozco.helium

Declara el nombre del paquete para este archivo, organizando el código dentro de la estructura del proyecto.

2. import ...

Importa todas las clases y funciones necesarias de las librerías de AndroidX y Jetpack Compose que se utilizarán en el archivo.

3. class MainActivity : ComponentActivity()

Define la clase MainActivity, que funciona como la pantalla de inicio y el punto de entrada principal de la aplicación. Hereda de ComponentActivity, que es la base para usar Jetpack Compose.

4. override fun onCreate(savedInstanceState: Bundle?)

Este es el método que se ejecuta cuando la actividad se crea por primera vez. Aquí se inicializa toda la configuración.

5. super.onCreate(savedInstanceState)

Llama a la implementación del mismo método en la clase padre (ComponentActivity) para asegurar que la inicialización básica se complete.

6. enableEdgeToEdge()

Habilita el modo "de borde a borde", que permite que la interfaz de la aplicación se dibuje detrás de las barras del sistema para una apariencia más inmersiva.

7. setContent { ... }

Establece el contenido de la interfaz de usuario de la actividad utilizando Jetpack Compose. Todo lo que está dentro de este bloque define la UI.

8. HeliumTheme { HeliumApp() }

Aplica un tema personalizado (HeliumTheme) a la aplicación (HeliumApp) para asegurar que todos los componentes de la UI tengan un estilo consistente.

9. @Composable

Es una anotación que marca una función como un componente de UI de Jetpack Compose. Estas funciones describen cómo debe ser una parte de la interfaz.

10. fun HeliumApp()

Define la función composable principal que construye la estructura visual de la aplicación.

11. val navController = rememberNavController()

Crea y "recuerda" una instancia de NavController. Este objeto es el cerebro de la navegación; gestiona qué pantalla se muestra y cómo se realizan las transiciones.

12. Scaffold(...)

Es un componente de Material 3 que provee una estructura de diseño estándar, ofreciendo espacios para elementos como barras de navegación y el contenido principal.

13. bottomBar = { ... }

Define el contenido que aparecerá en la parte inferior de la pantalla, usado aquí para la barra de navegación.

14. NavigationBar { ... }

Componente de Material 3 que crea la barra de navegación inferior.

15. val navBackStackEntry by ...

Observa el estado de la pila de navegación. navBackStackEntry se actualiza automáticamente cada vez que se navega a una nueva pantalla.

16. val currentDestination = ...

Obtiene la información del destino actual (la pantalla visible) desde la pila de navegación.

17. bottomNavItems.forEach { ... }

Recorre una lista predefinida (bottomNavItems) para crear dinámicamente cada elemento en la barra de navegación.

18. NavigationBarItem(...)

Crea un único elemento clickable dentro de la NavigationBar.

19. selected = ...

Determina si este ítem debe aparecer "seleccionado", comprobando si la ruta de la pantalla actual coincide con la ruta de este ítem.

20. onClick = { ... }

Define la acción que se ejecuta al hacer clic en el ítem: navegar a la ruta asociada.

21. popUpTo(...), launchSingleTop, restoreState

Son opciones de navegación clave que optimizan la experiencia, evitando acumular pantallas repetidas y restaurando el estado.

22. icon = { ... }, label = { ... }

Definen el ícono y el texto que se mostrarán para cada NavigationBarItem.

23. ) { innerPadding -> ... }

Es el bloque de contenido principal del Scaffold. El parámetro innerPadding evita que el contenido se superponga con las barras.

24. Box(modifier = Modifier.padding(innerPadding))

Un contenedor simple que aplica el relleno recibido del Scaffold, asegurando que el contenido se muestre en el área correcta.

25. NavHost(...)

Es el componente central que gestiona qué pantalla se muestra, intercambiando su contenido según las instrucciones del navController.

26. navController = ..., startDestination = ...

Conecta el NavHost con el navController y establece la pantalla de inicio de la aplicación.

27. composable(...) { ... }

Define una ruta de navegación. Cuando se navega a esa ruta, muestra el contenido de la función composable correspondiente (ej. HomeScreen()).

A continuación, se presenta el código fuente del archivo Navigation.kt. Este archivo es fundamental para la arquitectura de la aplicación "Helium", ya que centraliza y define toda la estructura de navegación. Utiliza una clase sellada (sealed class) para declarar cada una de las pantallas de la aplicación de una manera segura y organizada, asociando a cada una su ruta, título e ícono. Finalmente, agrupa estas pantallas en una lista que será utilizada por la interfaz de usuario para construir la barra de navegación inferior.

Helium: Estructura de Navegación (Navigation.kt)

package com.felipeorozco.helium.ui.navigation

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ListAlt
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Share
import androidx.compose.ui.graphics.vector.ImageVector

sealed class AppScreens(val route: String, val title: String, val icon: ImageVector) {
    object Home : AppScreens("home", "Inicio", Icons.Default.Home)
    object Network : AppScreens("network", "Red", Icons.Default.Share)
    object Activity : AppScreens("activity", "Actividad", Icons.AutoMirrored.Filled.ListAlt)
    object Settings : AppScreens("settings", "Ajustes", Icons.Default.Settings)
}

val bottomNavItems = listOf(
    AppScreens.Home,
    AppScreens.Network,
    AppScreens.Activity,
    AppScreens.Settings
)

Explicación del código: Helium: Estructura de Navegación (Navigation.kt)

1. package com.felipeorozco.helium.ui.navigation

Define la ubicación del archivo dentro de la estructura del proyecto, indicando que pertenece a la capa de interfaz de usuario (ui) y específicamente a la parte de navegación.

2. import ...

Importa los componentes necesarios. En este caso, los íconos de Material Design (Icons) y la clase ImageVector que se usa para representarlos gráficamente.

3. sealed class AppScreens(...)

Declara una clase sellada llamada AppScreens. Este tipo de clase restringe la herencia, permitiendo crear un conjunto fijo y conocido de subclases (en este caso, las pantallas de la app). Cada instancia de AppScreens debe tener una ruta (route), un título (title) y un ícono (icon).

4. object Home : AppScreens(...)

Define la primera pantalla como un objeto singleton. Home es una instancia única de AppScreens que representa la pantalla de "Inicio", con su ruta "home" y el ícono correspondiente.

5. object Network, object Activity, object Settings

De la misma manera que Home, estos objetos definen las otras tres pantallas de la aplicación: Red, Actividad y Ajustes, cada una con su propia ruta, título e ícono específico.

6. val bottomNavItems = listOf(...)

Crea una lista inmutable llamada bottomNavItems que contiene las cuatro pantallas definidas anteriormente. Esta lista será usada en la UI (en MainActivity) para generar dinámicamente los elementos de la barra de navegación inferior, asegurando consistencia y facilitando el mantenimiento.

HomeScreen.kt

A continuación, se presenta el código fuente del archivo HomeScreen.kt, que define la interfaz de usuario de la pantalla principal de la aplicación "Helium". Este componente, construido enteramente con Jetpack Compose, está diseñado de forma modular, dividiendo la pantalla en subcomponentes reutilizables como la cabecera, tarjetas de información y gráficos. Se utiliza una LazyColumn para garantizar un desplazamiento fluido y eficiente, y un Canvas para dibujar un gráfico de barras personalizado. El archivo también incluye las estructuras de datos (data classes) que definen el estado de la pantalla y vistas previas (previews) para visualizar el diseño en temas claro y oscuro.

Helium: Código de la Pantalla de Inicio (HomeScreen.kt)

package com.felipeorozco.helium.ui.screens

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.felipeorozco.helium.ui.theme.HeliumTheme
import kotlin.random.Random

data class HomeScreenState(
    val userName: String = "Felipe",
    val greeting: String = "Jueves por la tarde",
    val totalSavings: String = "1.72 GB",
    val networkStatus: String = "Red Estable",
    val activePeers: Int = 3,
    val dailySavingsData: List<Float> = fakeDailySavings,
    val ongoingUpdates: List<AppUpdateInfo> = fakeUpdateList
)

data class AppUpdateInfo(
    val appName: String,
    val totalSize: String,
    val overallProgress: Float,
    val criticalProgress: Float
)

@Composable
fun HomeScreen() {
    val state = HomeScreenState()

    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background)
    ) {
        item {
            HomeHeader(state)
            Spacer(Modifier.height(16.dp))
        }

        item {
            SavingsHeroCard(state)
            Spacer(Modifier.height(24.dp))
        }

        item {
            DailySavingsChart(state.dailySavingsData)
            Spacer(Modifier.height(24.dp))
        }

        item {
            StatusGrid(state)
            Spacer(Modifier.height(24.dp))
        }

        item {
            Text(
                "Actualizaciones Activas",
                style = MaterialTheme.typography.titleLarge,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(horizontal = 24.dp)
            )
            Spacer(Modifier.height(8.dp))
        }
        items(state.ongoingUpdates, key = { it.appName }) { update ->
            UpdateListItem(update = update)
        }
    }
}

@Composable
fun HomeHeader(state: HomeScreenState) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 24.dp, end = 24.dp, top = 24.dp)
    ) {
        Text(
            text = "Hola, ${state.userName}",
            style = MaterialTheme.typography.titleLarge,
            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
        )
        Text(
            text = state.greeting,
            style = MaterialTheme.typography.headlineMedium,
            fontWeight = FontWeight.Bold
        )
    }
}

@Composable
fun SavingsHeroCard(state: HomeScreenState) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 24.dp),
        shape = RoundedCornerShape(28.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
    ) {
        Box(
            modifier = Modifier.background(
                Brush.linearGradient(
                    colors = listOf(
                        MaterialTheme.colorScheme.primary,
                        MaterialTheme.colorScheme.secondary
                    )
                )
            )
        ) {
            Column(Modifier.padding(24.dp)) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Icon(
                        Icons.Default.DataSaverOn,
                        contentDescription = "Ahorro total",
                        tint = MaterialTheme.colorScheme.onPrimary
                    )
                    Spacer(Modifier.width(8.dp))
                    Text(
                        "AHORRO TOTAL",
                        style = MaterialTheme.typography.labelMedium,
                        color = MaterialTheme.colorScheme.onPrimary
                    )
                }
                Spacer(Modifier.height(8.dp))
                Text(
                    text = state.totalSavings,
                    style = MaterialTheme.typography.displayMedium,
                    fontWeight = FontWeight.ExtraBold,
                    color = MaterialTheme.colorScheme.onPrimary
                )
                Spacer(Modifier.height(16.dp))
                Button(
                    onClick = { /* TODO: Navegar a pantalla de Actividad */ },
                    shape = CircleShape,
                    colors = ButtonDefaults.buttonColors(
                        containerColor = MaterialTheme.colorScheme.onPrimary,
                        contentColor = MaterialTheme.colorScheme.primary
                    )
                ) {
                    Text("Ver detalles")
                    Icon(
                        Icons.AutoMirrored.Filled.ArrowForward,
                        contentDescription = null,
                        modifier = Modifier.size(18.dp)
                    )
                }
            }
        }
    }
}

@Composable
fun DailySavingsChart(data: List<Float>) {
    val primaryColor = MaterialTheme.colorScheme.primary
    val trackColor = MaterialTheme.colorScheme.surfaceVariant

    Column(modifier = Modifier.padding(horizontal = 24.dp)) {
        Text("Ahorro últimos 7 días", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
        Spacer(Modifier.height(16.dp))
        Canvas(
            modifier = Modifier
                .fillMaxWidth()
                .height(100.dp)
        ) {
            val barWidth = (size.width - (data.size - 1) * 8.dp.toPx()) / data.size
            data.forEachIndexed { index, value ->
                drawRoundRect(
                    color = trackColor,
                    topLeft = Offset(x = index * (barWidth + 8.dp.toPx()), y = 0f),
                    size = Size(width = barWidth, height = size.height),
                    cornerRadius = CornerRadius(x = 16f, y = 16f)
                )
                drawRoundRect(
                    color = primaryColor,
                    topLeft = Offset(x = index * (barWidth + 8.dp.toPx()), y = size.height * (1 - value)),
                    size = Size(width = barWidth, height = size.height * value),
                    cornerRadius = CornerRadius(x = 16f, y = 16f)
                )
            }
        }
    }
}

@Composable
fun StatusGrid(state: HomeScreenState) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 24.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        StatusInfoCard(
            modifier = Modifier.weight(1f),
            icon = Icons.Default.SignalCellularAlt,
            label = "Estado de Red",
            value = state.networkStatus
        )
        StatusInfoCard(
            modifier = Modifier.weight(1f),
            icon = Icons.Default.People,
            label = "Pares Activos",
            value = state.activePeers.toString()
        )
    }
}

@Composable
fun StatusInfoCard(modifier: Modifier = Modifier, icon: ImageVector, label: String, value: String) {
    Card(
        modifier = modifier,
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
    ) {
        Column(Modifier.padding(16.dp)) {
            Icon(
                imageVector = icon,
                contentDescription = label,
                tint = MaterialTheme.colorScheme.primary,
                modifier = Modifier.size(24.dp)
            )
            Spacer(Modifier.height(8.dp))
            Text(label, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
            Text(value, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant)
        }
    }
}

@Composable
fun UpdateListItem(update: AppUpdateInfo) {
    ListItem(
        modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
        headlineContent = { Text(update.appName, fontWeight = FontWeight.SemiBold) },
        supportingContent = {
            Column(Modifier.padding(top = 4.dp)) {
                LinearProgressIndicator(
                    progress = update.overallProgress,
                    modifier = Modifier
                        .fillMaxWidth()
                        .clip(CircleShape),
                    color = MaterialTheme.colorScheme.primary,
                    trackColor = MaterialTheme.colorScheme.surfaceVariant
                )
                Text(
                    "Crítico: ${(update.criticalProgress * 100).toInt()}%",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
                    modifier = Modifier.padding(top = 4.dp)
                )
            }
        },
        leadingContent = {
            Box(
                modifier = Modifier
                    .size(48.dp)
                    .clip(CircleShape)
                    .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
                contentAlignment = Alignment.Center
            ) {
                Icon(
                    Icons.Default.Download,
                    contentDescription = update.appName,
                    tint = MaterialTheme.colorScheme.primary,
                    modifier = Modifier.size(24.dp)
                )
            }
        },
        trailingContent = {
            Text(
                "${(update.overallProgress * 100).toInt()}%",
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.Bold
            )
        },
        colors = ListItemDefaults.colors(containerColor = Color.Transparent)
    )
}


val fakeDailySavings = List(7) { Random.nextFloat() }
val fakeUpdateList = listOf(
    AppUpdateInfo("Google Chrome", "124 MB", 0.75f, 1.0f),
    AppUpdateInfo("WhatsApp Messenger", "88 MB", 0.4f, 0.6f),
    AppUpdateInfo("Spotify", "96 MB", 0.25f, 0.3f)
)

@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_NO)
@Composable
fun HomeScreenLightPreview() {
    HeliumTheme {
        HomeScreen()
    }
}

@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
@Composable
fun HomeScreenDarkPreview() {
    HeliumTheme(darkTheme = true) {
        HomeScreen()
    }
}

Explicación del código: Helium: Código de la Pantalla de Inicio (HomeScreen.kt)

1. package e import

Declaran la ubicación del archivo y traen todas las herramientas de Jetpack Compose necesarias, como componentes de UI (Card, Text), layouts (Column, Row), íconos y modificadores.

2. data class HomeScreenState

Define una clase de datos que agrupa toda la información que la pantalla necesita mostrar. Este patrón ayuda a mantener el estado organizado y facilita la gestión de los datos en un solo lugar.

3. data class AppUpdateInfo

Modela la información específica para un solo elemento en la lista de "Actualizaciones Activas", conteniendo su nombre, tamaño y progreso.

4. @Composable fun HomeScreen()

Es la función principal que construye toda la pantalla. Usa una LazyColumn, que es la versión eficiente de Compose para listas con scroll, ya que solo renderiza los elementos visibles en pantalla. Organiza la UI llamando a otros componentes más pequeños.

5. @Composable fun HomeHeader()

Un componente simple y reutilizable que se encarga de mostrar el saludo de bienvenida al usuario en la parte superior de la pantalla.

6. @Composable fun SavingsHeroCard()

Crea la tarjeta principal (o "héroe") que muestra el ahorro total de datos. Destaca por su diseño con un fondo de gradiente (Brush.linearGradient) y un botón con forma de círculo (CircleShape) para ver más detalles.

7. @Composable fun DailySavingsChart()

Este es uno de los componentes más avanzados. Utiliza el Canvas de Compose para dibujar un gráfico de barras personalizado desde cero, demostrando la flexibilidad de la librería para crear cualquier tipo de elemento visual.

8. @Composable fun StatusGrid() y StatusInfoCard()

Estos dos componentes trabajan juntos. StatusGrid usa una Row para colocar dos StatusInfoCard una al lado de la otra. El modificador .weight(1f) es clave para que ambas tarjetas compartan el espacio disponible de manera equitativa.

9. @Composable fun UpdateListItem()

Construye cada fila de la lista de actualizaciones. Usa componentes estándar de Material 3 como ListItem y LinearProgressIndicator para mostrar la información del progreso de descarga de manera clara y consistente.

10. fakeDailySavings y fakeUpdateList

Estas variables crean datos de ejemplo (falsos). Son extremadamente útiles durante el desarrollo para poder construir y previsualizar la interfaz de usuario sin necesidad de conectarse a una fuente de datos real.

11. @Preview(...)

La anotación @Preview es una herramienta poderosa de Jetpack Compose. Permite renderizar y visualizar el componente directamente en el editor de Android Studio. En este caso, se definen dos vistas previas (HomeScreenLightPreview y HomeScreenDarkPreview) para verificar cómo se ve la pantalla tanto en tema claro como en oscuro, agilizando el proceso de diseño.

NetworkScreen.kt

A continuación, se presenta el código fuente del archivo NetworkScreen.kt. Este archivo define la pantalla "Red" de la aplicación "Helium", encargada de visualizar el estado de la red y los dispositivos cercanos. Es un ejemplo avanzado de Jetpack Compose que incluye un radar animado dibujado a medida con Canvas, una animación de pulso infinito creada con LaunchedEffect, y un layout completamente personalizado (DevicePositionLayout) que posiciona los íconos de los dispositivos usando cálculos trigonométricos. La lógica está organizada con data classes y una clase enum para gestionar los diferentes estados de los pares, y como siempre, incluye vistas previas para los temas claro y oscuro.

Helium: Código de la Pantalla de Red (NetworkScreen.kt)

package com.felipeorozco.helium.ui.screens

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ConnectingAirports
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import com.felipeorozco.helium.ui.theme.HeliumTheme
import kotlin.math.cos
import kotlin.math.sin

data class NetworkScreenState(
    val networkSummary: String = "Cobertura Fuerte",
    val detectedPeersCount: Int = 5,
    val peersOnRadar: List<PeerOnRadar> = fakePeersOnRadar,
    val connectedPeers: List<ConnectedPeer> = fakeConnectedPeers
)

data class PeerOnRadar(val id: String, val distance: Float, val angle: Float)
data class ConnectedPeer(val id: String, val deviceName: String, val status: PeerStatus, val signalStrength: Int)

enum class PeerStatus(val text: String, val icon: ImageVector, val color: @Composable () -> Color) {
    CONNECTED("Estable", Icons.Default.CheckCircle, { MaterialTheme.colorScheme.primary }),
    TRANSFERRING("Transfiriendo", Icons.Default.Sync, { MaterialTheme.colorScheme.secondary }),
    CONNECTING("Conectando", Icons.Default.ConnectingAirports, { MaterialTheme.colorScheme.tertiary })
}

@Composable
fun NetworkScreen() {
    val state = NetworkScreenState()

    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background)
    ) {
        item {
            NetworkHeader(state)
            Spacer(Modifier.height(16.dp))
        }

        item {
            AnimatedRadar(peers = state.peersOnRadar)
            Spacer(Modifier.height(24.dp))
        }

        item {
            Text(
                "Pares Conectados (${state.connectedPeers.size})",
                style = MaterialTheme.typography.titleLarge,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(horizontal = 24.dp)
            )
            Spacer(Modifier.height(8.dp))
        }

        items(state.connectedPeers, key = { it.id }) { peer ->
            PeerListItem(peer = peer)
        }
    }
}

@Composable
fun NetworkHeader(state: NetworkScreenState) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 24.dp, end = 24.dp, top = 24.dp)
    ) {
        Text(
            "Estado de la Red",
            style = MaterialTheme.typography.headlineMedium,
            fontWeight = FontWeight.Bold
        )
        Text(
            "${state.networkSummary} | ${state.detectedPeersCount} dispositivos detectados",
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.primary
        )
    }
}

@Composable
fun AnimatedRadar(peers: List<PeerOnRadar>) {
    val pulseAnim = remember { Animatable(0f) }
    LaunchedEffect(Unit) {
        pulseAnim.animateTo(
            1f,
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis = 2000, delayMillis = 500),
                repeatMode = RepeatMode.Restart
            )
        )
    }

    val primaryColor = MaterialTheme.colorScheme.primary

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .aspectRatio(1f)
            .padding(32.dp),
        contentAlignment = Alignment.Center
    ) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val radius = size.minDimension / 2
            val center = Offset(size.width / 2, size.height / 2)

            drawCircle(
                brush = Brush.radialGradient(
                    colors = listOf(primaryColor.copy(alpha = 0.05f), Color.Transparent),
                    center = center,
                    radius = radius
                ),
                radius = radius,
                center = center
            )

            drawCircle(
                color = primaryColor.copy(alpha = 1 - pulseAnim.value),
                radius = radius * pulseAnim.value,
                center = center,
                style = Stroke(width = (2.dp.toPx() * (1 - pulseAnim.value)))
            )

            repeat(3) { i ->
                drawCircle(
                    color = primaryColor.copy(alpha = 0.2f),
                    radius = radius * (i + 1) / 3,
                    center = center,
                    style = Stroke(width = 1.dp.toPx())
                )
            }

            drawCircle(color = primaryColor, radius = 8.dp.toPx(), center = center)
        }

        DevicePositionLayout(peers = peers) {
            peers.forEach {
                Icon(
                    Icons.Default.Smartphone,
                    contentDescription = "Peer ${it.id}",
                    tint = MaterialTheme.colorScheme.primary,
                    modifier = Modifier.size(24.dp)
                )
            }
        }
    }
}

@Composable
fun DevicePositionLayout(
    peers: List<PeerOnRadar>,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        val placeables = measurables.map { it.measure(Constraints()) }
        layout(constraints.maxWidth, constraints.maxHeight) {
            val radius = constraints.maxWidth / 2
            val center = Offset(constraints.maxWidth / 2f, constraints.maxHeight / 2f)

            placeables.forEachIndexed { index, placeable ->
                val peer = peers[index]
                val angleRad = Math.toRadians(peer.angle.toDouble())
                val x = center.x + (radius * peer.distance * cos(angleRad)).toFloat() - placeable.width / 2
                val y = center.y + (radius * peer.distance * sin(angleRad)).toFloat() - placeable.height / 2
                placeable.place(x.toInt(), y.toInt())
            }
        }
    }
}

@Composable
fun PeerListItem(peer: ConnectedPeer) {
    ListItem(
        modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
        headlineContent = { Text(peer.deviceName, fontWeight = FontWeight.SemiBold) },
        supportingContent = { Text("Señal: ${peer.signalStrength} dBm") },
        leadingContent = {
            Box(
                modifier = Modifier
                    .size(40.dp)
                    .clip(CircleShape)
                    .background(peer.status.color().copy(alpha = 0.1f)),
                contentAlignment = Alignment.Center
            ) {
                Icon(
                    imageVector = peer.status.icon,
                    contentDescription = peer.status.text,
                    tint = peer.status.color(),
                    modifier = Modifier.size(20.dp)
                )
            }
        },
        trailingContent = {
            Text(peer.status.text, color = peer.status.color(), fontWeight = FontWeight.Medium)
        },
        colors = ListItemDefaults.colors(containerColor = Color.Transparent)
    )
}

val fakePeersOnRadar = listOf(
    PeerOnRadar("p1", 0.8f, 45f), PeerOnRadar("p2", 0.5f, 150f),
    PeerOnRadar("p3", 0.9f, 280f), PeerOnRadar("p4", 0.3f, 210f), PeerOnRadar("p5", 0.7f, 320f)
)
val fakeConnectedPeers = listOf(
    ConnectedPeer("peer1", "Helium-Node-Gamma", PeerStatus.TRANSFERRING, -45),
    ConnectedPeer("peer2", "Helium-Node-Delta", PeerStatus.CONNECTED, -58),
    ConnectedPeer("peer3", "Helium-Node-Alpha", PeerStatus.CONNECTING, -65)
)

@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_NO)
@Composable
fun NetworkScreenLightPreview() {
    HeliumTheme { NetworkScreen() }
}
@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
@Composable
fun NetworkScreenDarkPreview() {
    HeliumTheme(darkTheme = true) { NetworkScreen() }
}

Explicación del código: Helium: Código de la Pantalla de Red (NetworkScreen.kt)

1. package e import

Declaran la ubicación del archivo e importan todas las herramientas necesarias de Jetpack Compose, incluyendo componentes de animación (Animatable), layouts (LazyColumn), íconos y modificadores.

2. data class ...

Se definen tres clases de datos: NetworkScreenState para agrupar toda la información de la pantalla, PeerOnRadar para modelar un dispositivo detectado en el radar (con distancia y ángulo), y ConnectedPeer para la lista de dispositivos ya conectados (con nombre, estado y señal).

3. enum class PeerStatus

Una clase enumerada que define los tres posibles estados de un par: CONNECTED, TRANSFERRING y CONNECTING. Este es un enfoque muy limpio, ya que cada estado contiene el texto, el ícono y el color que le corresponden en la UI.

4. @Composable fun NetworkScreen()

La función principal que construye la pantalla completa. Utiliza una LazyColumn para asegurar un rendimiento óptimo al mostrar listas de elementos que pueden crecer.

5. @Composable fun NetworkHeader()

Un componente simple que muestra el título y un resumen del estado de la red en la parte superior de la pantalla.

6. @Composable fun AnimatedRadar()

El corazón visual de la pantalla. Usa un Canvas para dibujar un radar a medida con círculos concéntricos y un gradiente. Además, utiliza LaunchedEffect y Animatable para crear una animación de pulso que se repite infinitamente, dando al radar una apariencia dinámica.

7. @Composable fun DevicePositionLayout()

Un componente de layout avanzado y totalmente personalizado. Utiliza el primitivo Layout de Compose para medir y posicionar cada ícono de dispositivo en el radar. Calcula las coordenadas X/Y exactas basándose en el ángulo y la distancia del par, usando las funciones trigonométricas cos y sin.

8. @Composable fun PeerListItem()

Define cómo se ve cada elemento en la lista de "Pares Conectados". Utiliza el componente estándar ListItem y obtiene el ícono y color correctos directamente del PeerStatus enum, lo que hace el código muy limpio y fácil de mantener.

9. fakePeersOnRadar y fakeConnectedPeers

Estas variables crean listas de datos de ejemplo. Son cruciales para poder desarrollar y previsualizar el complejo diseño del radar y la lista de pares sin depender de datos de red en tiempo real.

10. @Preview(...)

Las anotaciones @Preview permiten visualizar esta pantalla compleja, con sus animaciones y layouts personalizados, directamente en Android Studio, tanto en tema claro como oscuro, facilitando enormemente el desarrollo y la depuración de la UI.

ActivityScreen.kt

A continuación, se presenta el código fuente del archivo ActivityScreen.kt. Este archivo construye la pantalla de "Actividad", que muestra un historial cronológico de los ahorros y contribuciones del usuario en un formato de línea de tiempo. La característica principal de esta pantalla es el uso de cabeceras fijas (sticky headers) dentro de una LazyColumn, lo que permite que las fechas se queden fijas en la parte superior mientras el usuario se desplaza por los eventos de ese día. La interfaz está construida con componentes modulares, incluyendo un nodo de línea de tiempo dibujado a medida, y utiliza una clase enum para gestionar los tipos de actividad de forma limpia y organizada.

Helium: Código de la Pantalla de Actividad (ActivityScreen.kt)

package com.felipeorozco.helium.ui.screens

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DownloadDone
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.felipeorozco.helium.ui.theme.HeliumTheme

enum class ActivityType(val icon: ImageVector, val color: @Composable () -> Color) {
    SAVING(Icons.Default.DownloadDone, { MaterialTheme.colorScheme.primary }),
    CONTRIBUTION(Icons.Default.Upload, { MaterialTheme.colorScheme.secondary })
}

data class ActivityItem(
    val id: Int,
    val type: ActivityType,
    val title: String,
    val description: String,
    val timestamp: String,
    val date: String
)

data class ActivityScreenState(
    val savingsToday: String = "197 MB",
    val contributionsToday: String = "42 MB",
    val groupedActivities: Map<String, List<ActivityItem>> = fakeActivityItems.groupBy { it.date }
)

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ActivityScreen() {
    val state = ActivityScreenState()

    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background),
        contentPadding = PaddingValues(vertical = 16.dp)
    ) {
        item {
            ActivityHeader()
            SummarySection(state)
        }

        state.groupedActivities.forEach { (date, activities) ->
            stickyHeader {
                DateHeader(date = date)
            }
            items(activities, key = { it.id }) { activity ->
                TimelineItem(item = activity, isLastItem = activities.last() == activity)
            }
        }
    }
}

@Composable
fun ActivityHeader() {
    Text(
        text = "Historial de Actividad",
        style = MaterialTheme.typography.headlineMedium,
        fontWeight = FontWeight.Bold,
        modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 16.dp)
    )
}

@Composable
fun SummarySection(state: ActivityScreenState) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 24.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        SummaryCard(
            modifier = Modifier.weight(1f),
            label = "Ahorrado Hoy",
            value = state.savingsToday,
            color = MaterialTheme.colorScheme.primary
        )
        SummaryCard(
            modifier = Modifier.weight(1f),
            label = "Contribuido Hoy",
            value = state.contributionsToday,
            color = MaterialTheme.colorScheme.secondary
        )
    }
}

@Composable
fun SummaryCard(modifier: Modifier = Modifier, label: String, value: String, color: Color) {
    Card(
        modifier = modifier,
        colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f))
    ) {
        Column(Modifier.padding(16.dp)) {
            Text(label, style = MaterialTheme.typography.labelMedium, color = color)
            Text(value, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = color)
        }
    }
}

@Composable
fun DateHeader(date: String) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = date,
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.Bold
        )
        HorizontalDivider(modifier = Modifier.padding(start = 16.dp))
    }
}

@Composable
fun TimelineItem(item: ActivityItem, isLastItem: Boolean) {
    Row(modifier = Modifier.padding(horizontal = 24.dp)) {
        TimelineNode(type = item.type, isLastItem = isLastItem)
        ActivityEventCard(item = item)
    }
}

@Composable
fun TimelineNode(type: ActivityType, isLastItem: Boolean) {
    val nodeColor = type.color()
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        // Línea superior (excepto para el primer item, que no la necesita aquí)
        Box(
            modifier = Modifier
                .height(12.dp)
                .width(2.dp)
                .background(MaterialTheme.colorScheme.surfaceVariant)
        )
        // El nodo del evento
        Box(
            modifier = Modifier
                .size(36.dp)
                .clip(CircleShape)
                .background(nodeColor.copy(alpha = 0.2f)),
            contentAlignment = Alignment.Center
        ) {
            Icon(
                imageVector = type.icon,
                contentDescription = type.name,
                tint = nodeColor,
                modifier = Modifier.size(20.dp)
            )
        }
        // Línea inferior, si no es el último item
        if (!isLastItem) {
            Box(
                modifier = Modifier
                    .fillMaxHeight()
                    .width(2.dp)
                    .background(MaterialTheme.colorScheme.surfaceVariant)
            )
        }
    }
}

@Composable
fun ActivityEventCard(item: ActivityItem) {
    Card(
        modifier = Modifier.padding(start = 16.dp, bottom = 16.dp).fillMaxWidth(),
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(item.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
            Spacer(Modifier.height(4.dp))
            Text(item.description, style = MaterialTheme.typography.bodyMedium)
            Spacer(Modifier.height(8.dp))
            Text(
                item.timestamp,
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
            )
        }
    }
}

val fakeActivityItems = listOf(
    ActivityItem(1, ActivityType.SAVING, "Actualización de YouTube", "Ahorraste 85 MB vía P2P", "2:15 PM", "Hoy, 24 de julio"),
    ActivityItem(2, ActivityType.CONTRIBUTION, "Contribución a la red", "Compartiste 42 MB con 2 pares", "1:30 PM", "Hoy, 24 de julio"),
    ActivityItem(3, ActivityType.SAVING, "Actualización de Chrome", "Ahorraste 112 MB vía P2P", "11:05 AM", "Hoy, 24 de julio"),
    ActivityItem(4, ActivityType.SAVING, "Actualización de WhatsApp", "Ahorraste 25 MB vía P2P", "10:20 PM", "Ayer, 23 de julio"),
    ActivityItem(5, ActivityType.CONTRIBUTION, "Contribución a la red", "Compartiste 150 MB con 5 pares", "4:00 PM", "Ayer, 23 de julio"),
    ActivityItem(6, ActivityType.SAVING, "Actualización del Sistema", "Ahorraste 350 MB vía P2P", "9:30 AM", "22 de julio")
)

@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_NO)
@Composable
fun ActivityScreenLightPreview() {
    HeliumTheme { ActivityScreen() }
}
@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ActivityScreenDarkPreview() {
    HeliumTheme(darkTheme = true) { ActivityScreen() }
}

Explicación del código: Helium: Código de la Pantalla de Actividad (ActivityScreen.kt)

1. enum class ActivityType

Define los tipos de actividad posibles (SAVING y CONTRIBUTION). Cada tipo lleva asociado su propio ícono y color, lo que permite que la UI se adapte automáticamente según el tipo de evento.

2. data class ...

Se definen dos clases de datos: ActivityItem para modelar un evento individual del historial y ActivityScreenState para contener todo el estado de la pantalla. Es notable que groupedActivities es un mapa que agrupa los eventos por fecha, simplificando enormemente la creación de la línea de tiempo seccionada.

3. @Composable fun ActivityScreen()

Es la función principal que ensambla la pantalla. Su característica más importante es el uso de stickyHeader dentro de LazyColumn. Esto hace que cada DateHeader se quede "pegado" en la parte superior de la pantalla mientras el usuario se desplaza por los eventos de esa fecha, mejorando la experiencia de usuario.

4. ActivityHeader() y SummarySection()

Estos componentes construyen la parte superior de la pantalla, mostrando el título principal "Historial de Actividad" y dos tarjetas de resumen con los totales del día.

5. @Composable fun DateHeader()

Define la apariencia de las cabeceras de fecha que se usan como stickyHeader. Muestra la fecha y una línea divisora horizontal.

6. @Composable fun TimelineItem()

Compone una fila completa de la línea de tiempo. Utiliza una Row para alinear horizontalmente el nodo visual de la línea (TimelineNode) y la tarjeta con la información del evento (ActivityEventCard).

7. @Composable fun TimelineNode()

Un componente de UI personalizado que dibuja la parte gráfica de la línea de tiempo. Usa simples Box con colores y tamaños específicos para crear la línea vertical y el círculo central con su ícono. inteligentemente, comprueba si es el último elemento (isLastItem) para decidir si debe dibujar la línea de conexión inferior.

8. @Composable fun ActivityEventCard()

Muestra la información detallada de un solo evento de actividad dentro de un componente Card, incluyendo título, descripción y hora.

9. val fakeActivityItems

Una lista de datos de ejemplo que simula un historial de eventos. Estos datos se agrupan por fecha en el ActivityScreenState para alimentar la línea de tiempo en las vistas previas.

10. @Preview(...)

Permite a los desarrolladores visualizar la pantalla completa, incluyendo la compleja lista con cabeceras fijas, directamente en el editor de Android Studio para los temas claro y oscuro.

SettingsScreen.kt

A continuación, se presenta el código fuente del archivo SettingsScreen.kt. Este archivo define la pantalla de "Ajustes" de la aplicación "Helium". Su diseño es un excelente ejemplo de modularidad en Jetpack Compose, ya que agrupa las configuraciones en secciones lógicas y reutiliza componentes personalizados como SettingsSectionCard, ActionRow y SwitchRow para mantener una interfaz consistente y fácil de mantener. Se demuestra el manejo de estado local e interactivo mediante remember { mutableStateOf(...) } para controlar elementos como el Slider y los Switch, una práctica fundamental en el desarrollo con Compose.

Helium: Código de la Pantalla de Ajustes (SettingsScreen.kt)

package com.felipeorozco.helium.ui.screens

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.felipeorozco.helium.ui.theme.HeliumTheme

data class SettingsState(
    val cacheLimitGB: Float = 2.0f,
    val currentCacheUsage: String = "780 MB",
    val isAdaptiveModeEnabled: Boolean = true,
    val receiveNotifications: Boolean = true,
    val appVersion: String = "1.0.0 (Build 1)"
)

@Composable
fun SettingsScreen() {
    val state = SettingsState()

    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        item {
            Text(
                "Ajustes",
                style = MaterialTheme.typography.headlineMedium,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(horizontal = 8.dp)
            )
        }

        item { CacheSettingsSection(state) }
        item { PowerSettingsSection(state) }
        item { AppInfoSection(state) }

        item {
            Text(
                "Hecho con ☕ en Yarumal, Colombia",
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 16.dp),
                textAlign = androidx.compose.ui.text.style.TextAlign.Center
            )
        }
    }
}

@Composable
fun CacheSettingsSection(state: SettingsState) {
    var sliderPosition by remember { mutableStateOf(state.cacheLimitGB) }

    SettingsSectionCard(
        title = "Almacenamiento",
        icon = Icons.Default.Storage
    ) {
        Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text("Límite del caché", fontWeight = FontWeight.SemiBold)
                Spacer(Modifier.weight(1f))
                Text(
                    "${"%.1f".format(sliderPosition)} GB",
                    color = MaterialTheme.colorScheme.primary,
                    fontWeight = FontWeight.Bold
                )
            }
            Slider(
                value = sliderPosition,
                onValueChange = { sliderPosition = it },
                valueRange = 1f..10f,
                steps = 8
            )
            Text(
                "Recomendado: 2.0 GB (suficiente para ~15 actualizaciones)",
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
            )
        }
        HorizontalDivider(Modifier.padding(vertical = 8.dp))
        ActionRow(
            title = "Limpiar caché ahora",
            subtitle = "Uso actual: ${state.currentCacheUsage}",
            icon = Icons.Default.DeleteSweep,
            onClick = { /* TODO */ }
        )
    }
}

@Composable
fun PowerSettingsSection(state: SettingsState) {
    var isAdaptiveEnabled by remember { mutableStateOf(state.isAdaptiveModeEnabled) }
    var areNotificationsEnabled by remember { mutableStateOf(state.receiveNotifications) }

    SettingsSectionCard(title = "Energía y Notificaciones", icon = Icons.Default.Notifications) {
        SwitchRow(
            title = "Modo Adaptativo",
            subtitle = "Reduce la actividad con batería baja",
            icon = Icons.Default.BatterySaver,
            isChecked = isAdaptiveEnabled,
            onCheckedChange = { isAdaptiveEnabled = it }
        )
        HorizontalDivider(Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp))
        SwitchRow(
            title = "Activar notificaciones",
            subtitle = "Avisos sobre ahorros y errores",
            icon = Icons.Default.CircleNotifications,
            isChecked = areNotificationsEnabled,
            onCheckedChange = { areNotificationsEnabled = it }
        )
    }
}

@Composable
fun AppInfoSection(state: SettingsState) {
    SettingsSectionCard(title = "Aplicación", icon = Icons.Default.Info) {
        ActionRow(title = "Versión de la app", subtitle = state.appVersion, icon = Icons.Default.Tag)
        HorizontalDivider(Modifier.padding(vertical = 4.dp, horizontal = 16.dp))
        ActionRow(title = "Enviar feedback", subtitle = "Reporta un error o sugiere una mejora", icon = Icons.Default.Feedback, onClick = {})
        HorizontalDivider(Modifier.padding(vertical = 4.dp, horizontal = 16.dp))
        ActionRow(title = "Licencias de código abierto", icon = Icons.Default.Description, onClick = {})
    }
}

@Composable
fun SettingsSectionCard(
    title: String,
    icon: ImageVector,
    content: @Composable ColumnScope.() -> Unit
) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Column {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Icon(icon, contentDescription = title, tint = MaterialTheme.colorScheme.primary)
                Spacer(Modifier.width(16.dp))
                Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
            }
            HorizontalDivider()
            content()
        }
    }
}

@Composable
fun ActionRow(
    title: String,
    subtitle: String? = null,
    icon: ImageVector,
    onClick: (() -> Unit)? = null
) {
    val modifier = if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
    Row(
        modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(icon, contentDescription = title, tint = MaterialTheme.colorScheme.onSurfaceVariant)
        Spacer(Modifier.width(16.dp))
        Column(Modifier.weight(1f)) {
            Text(title, style = MaterialTheme.typography.bodyLarge)
            subtitle?.let {
                Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f))
            }
        }
    }
}

@Composable
fun SwitchRow(
    title: String,
    subtitle: String,
    icon: ImageVector,
    isChecked: Boolean,
    onCheckedChange: (Boolean) -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onCheckedChange(!isChecked) }
            .padding(horizontal = 16.dp, vertical = 8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(icon, contentDescription = title, tint = MaterialTheme.colorScheme.onSurfaceVariant)
        Spacer(Modifier.width(16.dp))
        Column(Modifier.weight(1f)) {
            Text(title, style = MaterialTheme.typography.bodyLarge)
            Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f))
        }
        Spacer(Modifier.width(16.dp))
        Switch(checked = isChecked, onCheckedChange = null) // null porque el Row ya maneja el click
    }
}

@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_NO)
@Composable
fun SettingsScreenLightPreview() {
    HeliumTheme { SettingsScreen() }
}

@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
@Composable
fun SettingsScreenDarkPreview() {
    HeliumTheme(darkTheme = true) { SettingsScreen() }
}

Explicación del código: Helium: Código de la Pantalla de Ajustes (SettingsScreen.kt)

1. data class SettingsState

Define una clase de datos que contiene todos los valores de configuración de la pantalla. Esto permite manejar el estado de una manera centralizada y predecible.

2. @Composable fun SettingsScreen()

Es la función principal que construye la pantalla de Ajustes. Utiliza una LazyColumn para presentar las diferentes secciones de configuración de forma vertical y eficiente.

3. CacheSettingsSection() y PowerSettingsSection()

Estos componentes agrupan configuraciones relacionadas. Notablemente, utilizan remember { mutableStateOf(...) } para crear y recordar el estado de los controles interactivos (el Slider y los Switch), permitiendo que la UI reaccione a la interacción del usuario.

4. AppInfoSection()

Esta sección muestra información estática sobre la aplicación, como la versión y enlaces a licencias, utilizando el componente reutilizable ActionRow.

5. @Composable fun SettingsSectionCard()

Un componente genérico y reutilizable que sirve como plantilla para cada tarjeta de sección. Acepta un título, un ícono y un bloque de contenido (lambda), lo que permite construir diferentes secciones con una apariencia visual consistente y muy poco código repetido.

6. @Composable fun ActionRow()

Define una fila reutilizable para mostrar una opción o información. inteligentemente, solo se vuelve clickable si se le proporciona una función onClick.

7. @Composable fun SwitchRow()

Un componente reutilizable para una fila que contiene un interruptor (Switch). Para mejorar la experiencia de usuario, toda la fila es clickable, no solo el interruptor.

8. @Preview(...)

Como en las otras pantallas, las vistas previas permiten visualizar el diseño de la pantalla de Ajustes en Android Studio para los temas claro y oscuro, facilitando el desarrollo rápido y la verificación visual de la UI.

Color.kt

A continuación, se presenta el código fuente del archivo Color.kt. Este archivo es el corazón del sistema de diseño de la aplicación "Helium", ya que define de manera centralizada toda la paleta de colores. Está organizado en dos esquemas principales, uno para el tema claro y otro para el tema oscuro, siguiendo las convenciones de Material Design 3 (Primary, Secondary, On Surface, etc.). Adicionalmente, define colores semánticos como Error y Warning. Este enfoque garantiza una consistencia visual total en toda la aplicación y facilita enormemente la gestión y actualización del tema.

Helium: Paleta de Colores (Color.kt)

Estás viendo solo el 60% del contenido. Hazte Premium para acceder al tutorial completo.

Comunidad

Comentarios y valoraciones

No hay comentarios aún. ¡Sé el primero en opinar!