tutoriales.com

¡Poder y Flexibilidad! Desarrollo de Widgets Interactivos en Android con Jetpack Glance

Este tutorial te guiará paso a paso en el desarrollo de widgets de aplicación para Android utilizando la biblioteca Jetpack Glance. Descubre cómo construir interfaces de usuario declarativas y reactivas que permitan a tus usuarios interactuar con tu aplicación directamente desde la pantalla de inicio, mejorando la usabilidad y el engagement.

Intermedio15 min de lectura17 views
Reportar error

Los widgets de aplicación son una herramienta poderosa para mejorar la interacción del usuario con tu aplicación Android. Permiten a los usuarios acceder a información clave o realizar acciones rápidas directamente desde la pantalla de inicio de su dispositivo, sin necesidad de abrir la aplicación completa. Tradicionalmente, desarrollar widgets ha sido complejo, pero con Jetpack Glance, el proceso se simplifica enormemente, trayendo el paradigma declarativo de Jetpack Compose a la creación de widgets.

En este tutorial, exploraremos Jetpack Glance a fondo, aprendiendo cómo diseñar, implementar y actualizar widgets interactivos. Cubriremos desde la configuración inicial hasta la creación de diseños complejos y la gestión de la interactividad.


🚀 ¿Qué es Jetpack Glance?

Jetpack Glance es un framework declarativo moderno de Jetpack que permite construir widgets de aplicación de Android de manera similar a Jetpack Compose. Utiliza un modelo de UI declarativo que simplifica la creación de interfaces de usuario para widgets, liberándonos de la complejidad de RemoteViews y los AppWidgetProvider tradicionales.

📌 Nota: Aunque Glance usa la sintaxis de Compose, no es Compose UI directamente. Compila a `RemoteViews`, que es lo que el sistema Android realmente entiende para renderizar widgets. Esto significa que no todas las funcionalidades de Compose están disponibles, pero las más importantes para widgets sí lo están.

Ventajas de Jetpack Glance:

  • Sintaxis declarativa: Escribe menos código y más legible, con un enfoque en cómo debe verse tu UI.
  • Reactividad: Los widgets se actualizan automáticamente cuando cambian los datos, similar a Compose.
  • Integración con Jetpack: Parte del ecosistema Jetpack, lo que facilita su uso con otras bibliotecas.
  • Menos boilerplate: Reduce la cantidad de código repetitivo necesario para configurar y actualizar widgets.

🛠️ Configuración del Entorno

Antes de sumergirnos en el código, necesitamos configurar nuestro proyecto Android para usar Jetpack Glance. Asegúrate de tener Android Studio actualizado y tu proyecto usando Kotlin.

1. Añadir dependencias

Primero, abre tu archivo build.gradle.kts (Module: app) y añade las siguientes dependencias en la sección dependencies:

dependencies {
    // Core Glance library
    implementation("androidx.glance:glance:1.0.0")
    implementation("androidx.glance:glance-appwidget:1.0.0")

    // Si necesitas imágenes, añade la dependencia de Glance para coil o Glide
    // implementation("androidx.glance:glance-appwidget-extension-coil:1.0.0")
    // implementation("androidx.glance:glance-appwidget-extension-glide:1.0.0")

    // Para composición asíncrona (opcional pero recomendado para tareas pesadas)
    // implementation("androidx.glance:glance-appwidget-extension-compose:1.0.0")
}
🔥 Importante: Verifica siempre la última versión estable de Glance en la documentación oficial de Android Developers para asegurar que usas la más reciente.

Sincroniza tu proyecto con los archivos Gradle una vez que hayas añadido las dependencias.


👶 Tu Primer Widget con Glance: "Hola Mundo" 🎉

Vamos a crear un widget simple que muestre un texto de "Hola Mundo".

1. Crear el AppWidgetProvider

Glance utiliza una clase que extiende GlanceAppWidget y GlanceAppWidgetReceiver en lugar del tradicional AppWidgetProvider. Primero, crea una clase para tu GlanceAppWidget. Este será el encargado de definir la interfaz de usuario de tu widget.

Crea un nuevo archivo Kotlin, por ejemplo, MyGlanceWidget.kt:

package com.example.myglanceapp

import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.provideContent
import androidx.glance.GlanceModifier
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.unit.dp

class MyGlanceWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: android.content.Context, id: androidx.glance.appwidget.AppWidgetId) {
        provideContent {
            // Definimos el contenido de nuestro widget
            Column(
                modifier = GlanceModifier.fillMaxSize().padding(8.dp)
            ) {
                Text("¡Hola, Glance Mundo!")
            }
        }
    }
}

class MyGlanceWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = MyGlanceWidget()
}

Explicación del Código:

  • MyGlanceWidget extiende GlanceAppWidget. La función provideGlance es donde defines la UI de tu widget usando composables de Glance.
  • provideContent es una función de extensión para GlanceAppWidget que toma un bloque Composable donde defines el diseño del widget. Aquí usamos Column y Text, que son similares a sus contrapartes en Jetpack Compose.
  • MyGlanceWidgetReceiver extiende GlanceAppWidgetReceiver. Esta es la clase que el sistema Android instanciará. Necesita saber qué GlanceAppWidget debe mostrar, por eso sobrescribimos glanceAppWidget.

2. Definir el AppWidgetProviderInfo

Necesitamos un archivo XML que describa las propiedades de nuestro widget. Este archivo se coloca en res/xml/.

Crea res/xml/my_glance_widget_info.xml:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minHeight="100dp"
    android:updatePeriodMillis="86400000" <!-- 24 horas -->
    android:initialLayout="@layout/glance_default_loading_layout"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:widgetFeatures="reconfigurable|disable_notify_on_app_stop"
    android:previewImage="@drawable/widget_preview"
    android:description="@string/app_widget_description">
</appwidget-provider>
💡 Consejo: La `previewImage` es crucial para que tu widget se vea bien en el selector de widgets. Crea un `drawable` para esto. El `initialLayout` se refiere a un layout tradicional que se muestra mientras el widget de Glance carga, aunque Glance a menudo gestiona esto internamente.

También necesitarás un string en res/values/strings.xml para la descripción:

<string name="app_widget_description">Un widget de ejemplo para aprender Glance.</string>

Y un drawable para la vista previa (por ejemplo, res/drawable/widget_preview.xml - puede ser un simple shape o una imagen de tu diseño):

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#C0C0C0" />
    <corners android:radius="8dp" />
</shape>

3. Registrar el Widget en AndroidManifest.xml

Finalmente, debemos registrar nuestro GlanceAppWidgetReceiver en el AndroidManifest.xml de nuestra aplicación. Esto permite que el sistema Android lo descubra.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <application ...>
        ...
        <receiver android:name=".MyGlanceWidgetReceiver"
            android:exported="false"> <!-- Considera exported="true" si otras apps necesitan interactuar con tu widget -->
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/my_glance_widget_info" />
        </receiver>
        ...
    </application>
</manifest>

¡Felicidades! 🎉 Ya puedes ejecutar tu aplicación y añadir el widget desde la pantalla de inicio de tu dispositivo o emulador. Busca el nombre de tu aplicación y verás tu widget de "Hola, Glance Mundo!".


🎨 Estilizando y Diseñando Widgets

Glance ofrece una serie de composables y modificadores para diseñar tus widgets. Aunque no tan potentes como Compose UI completo, son suficientes para la mayoría de los casos de uso de widgets.

Comprobables de diseño básicos:

Glance proporciona composables como Column, Row, Box, Text, Image, Button y LazyColumn (para listas). Utilízalos de manera similar a Compose.

Modificadores:

Los modificadores de Glance (GlanceModifier) te permiten añadir padding, background, width, height, y manejar clics (clickable).

Vamos a mejorar nuestro widget para que muestre un fondo de color y tenga un poco más de estilo.

Modifica MyGlanceWidget.kt:

package com.example.myglanceapp

import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.provideContent
import androidx.glance.GlanceModifier
import androidx.glance.background
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.wrapContentHeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.dp
import androidx.compose.ui.graphics.Color // Importa Color de Compose UI
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
import androidx.glance.layout.Alignment

class MyGlanceWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: android.content.Context, id: androidx.glance.appwidget.AppWidgetId) {
        provideContent {
            Column(
                modifier = GlanceModifier
                    .fillMaxSize()
                    .background(Color(0xFFE0F7FA)) // Un azul claro de fondo
                    .padding(16.dp),
                horizontalAlignment = Alignment.Horizontal.CenterHorizontally,
                verticalAlignment = Alignment.Vertical.CenterVertically
            ) {
                Text(
                    text = "Mi Widget Estilizado",
                    style = TextStyle(
                        fontWeight = FontWeight.Bold,
                        fontSize = 20.sp,
                        color = Color.Black
                    ),
                    modifier = GlanceModifier.padding(bottom = 8.dp)
                )
                Text(
                    text = "¡Información clave aquí!",
                    style = TextStyle(
                        fontSize = 14.sp,
                        color = Color(0xFF424242)
                    )
                )
            }
        }
    }
}
// MyGlanceWidgetReceiver remains the same as before
class MyGlanceWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = MyGlanceWidget()
}

Hemos añadido un color de fondo, centrado el contenido y aplicado estilos de texto personalizados usando TextStyle y GlanceModifier.

💡 Consejo: Los colores y unidades (`dp`, `sp`) se toman de `androidx.compose.ui.graphics.Color` y `androidx.compose.ui.unit`. Asegúrate de importarlos correctamente.

🔄 Actualizando Datos y Estado del Widget

Los widgets son más útiles cuando muestran información dinámica. Glance facilita la actualización del contenido de un widget.

1. Estado del Widget

Glance puede almacenar un estado para tu widget, lo que te permite actualizar la UI en función de ese estado. Esto se hace usando GlanceStateDefinition.

Crea un archivo de datos (data class) que represente el estado de tu widget, por ejemplo, WidgetState.kt:

package com.example.myglanceapp

import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.core.DataStore
import androidx.glance.state.GlanceStateDefinition
import java.io.File

object CounterStateDefinition : GlanceStateDefinition<Preferences> {
    private const val PREFS_FILE_NAME = "widget_prefs"
    val countKey = stringPreferencesKey("count_key")

    override suspend fun getDataStore(context: android.content.Context, fileKey: String): DataStore<Preferences> {
        // Usa DataStore para almacenar el estado del widget
        return androidx.datastore.preferences.preferencesDataStoreFile(context, PREFS_FILE_NAME)
    }

    override fun getLocation(context: android.content.Context, fileKey: String): File {
        // Retorna la ubicación del archivo de preferencias
        return File(context.applicationContext.filesDir, "datastore/$PREFS_FILE_NAME.preferences_pb")
    }
}

Ahora, modifica tu MyGlanceWidget para usar este estado. Vamos a crear un contador simple.

package com.example.myglanceapp

import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.provideContent
import androidx.glance.GlanceModifier
import androidx.glance.background
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.dp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
import androidx.glance.layout.Alignment
import androidx.glance.action.clickable
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.currentState
import androidx.glance.state.updateAppWidgetState
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.glance.Button
import androidx.glance.ButtonDefaults
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.width
import androidx.glance.action.actionStartActivity
import android.content.Intent

// Definir la clave para el contador
val countKey = intPreferencesKey("count_key")

class MyGlanceWidget : GlanceAppWidget() {

    override val stateDefinition = CounterStateDefinition // Asociamos nuestro estado personalizado

    override suspend fun provideGlance(context: android.content.Context, id: androidx.glance.appwidget.AppWidgetId) {
        // Leemos el estado actual del widget
        val prefs = currentState<Preferences>()
        val currentCount = prefs[countKey] ?: 0

        provideContent {
            Column(
                modifier = GlanceModifier
                    .fillMaxSize()
                    .background(Color(0xFFE8F5E9)) // Un verde claro de fondo
                    .padding(16.dp),
                horizontalAlignment = Alignment.Horizontal.CenterHorizontally,
                verticalAlignment = Alignment.Vertical.CenterVertically
            ) {
                Text(
                    text = "Contador: $currentCount",
                    style = TextStyle(
                        fontWeight = FontWeight.Bold,
                        fontSize = 24.sp,
                        color = Color.Black
                    ),
                    modifier = GlanceModifier.padding(bottom = 16.dp)
                )
                Row(
                    horizontalAlignment = Alignment.Horizontal.CenterHorizontally
                ) {
                    Button(
                        text = "Incrementar",
                        onClick = actionRunCallback<IncrementActionCallback>()
                    )
                    Spacer(GlanceModifier.width(8.dp))
                    Button(
                        text = "Abrir App",
                        onClick = actionStartActivity(Intent(context, MainActivity::class.java))
                    )
                }
            }
        }
    }
}

class MyGlanceWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = MyGlanceWidget()
}

2. Acciones e Interactividad

Para que el widget sea interactivo, necesitamos manejar los clics. Glance utiliza ActionCallback para esto.

Crea una nueva clase para la acción de incremento:

package com.example.myglanceapp

import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.update
import androidx.glance.state.updateAppWidgetState
import androidx.datastore.preferences.core.Preferences

class IncrementActionCallback : ActionCallback {
    override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
        updateAppWidgetState(context, glanceId) { prefs ->
            val currentCount = prefs[countKey] ?: 0
            prefs[countKey] = currentCount + 1
        }
        // Es crucial actualizar el widget después de cambiar el estado
        MyGlanceWidget().update(context, glanceId)
    }
}

¡Importante! Necesitas una MainActivity básica si aún no la tienes, ya que estamos usando actionStartActivity.

package com.example.myglanceapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                Text("¡Bienvenido a la App!")
            }
        }
    }
}

Ahora, cuando añades el widget a la pantalla de inicio y haces clic en el botón "Incrementar", el contador se actualizará. Al hacer clic en "Abrir App", se lanzará tu MainActivity.

⚠️ Advertencia: Las acciones en widgets son limitadas. No puedes ejecutar lógica compleja directamente en el widget; en su lugar, utiliza `ActionCallback` para operaciones que actualizan el estado del widget o lanzan componentes de tu aplicación.

Actualización Automática del Widget

Para que tu widget se actualice automáticamente (por ejemplo, para mostrar información en tiempo real), puedes usar WorkManager o simplemente configurar updatePeriodMillis en tu appwidget-provider XML. Sin embargo, para datos más complejos o actualizaciones activadas por eventos externos, WorkManager es la opción preferida.

Ejemplo de actualización con WorkManager

Primero, añade la dependencia de WorkManager en tu build.gradle.kts:

implementation("androidx.work:work-runtime-ktx:2.9.0")

Luego, crea un Worker que actualice el widget:

package com.example.myglanceapp

import android.content.Context
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.updateAll
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters

class WidgetUpdateWorker(appContext: Context, workerParams: WorkerParameters) :
    CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        val manager = GlanceAppWidgetManager(appContext)
        val glanceIds = manager.getGlanceIds(MyGlanceWidget::class.java)

        glanceIds.forEach { glanceId ->
            MyGlanceWidget().update(appContext, glanceId)
        }

        return Result.success()
    }
}

Finalmente, programa este Worker en tu MyGlanceWidgetReceiver cuando se añada el widget por primera vez:

package com.example.myglanceapp

import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit

class MyGlanceWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = MyGlanceWidget()

    override fun onEnabled(context: Context) {
        super.onEnabled(context)
        // Programa un trabajo periódico para actualizar el widget cada 15 minutos
        val workRequest = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(15, TimeUnit.MINUTES)
            .build()
        WorkManager.getInstance(context).enqueueUniquePeriodicWork(
            "WidgetUpdateWork",
            ExistingPeriodicWorkPolicy.UPDATE,
            workRequest
        )
    }

    override fun onDisabled(context: Context) {
        super.onDisabled(context)
        // Cancela el trabajo cuando el último widget es eliminado
        WorkManager.getInstance(context).cancelUniqueWork("WidgetUpdateWork")
    }
}

🖼️ Añadiendo Imágenes y Listas Desplazables

Glance también soporta imágenes y listas, aunque con ciertas limitaciones en comparación con Compose UI completo.

Imágenes

Para mostrar imágenes, necesitarás la extensión de Glance para Coil o Glide. Asumamos que usas Coil (si la añadiste en la sección de dependencias).

import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.layout.ContentScale
// ... dentro de provideContent

Image(
    provider = ImageProvider(R.drawable.my_icon),
    contentDescription = "Descripción de mi icono",
    modifier = GlanceModifier.width(48.dp).height(48.dp),
    contentScale = ContentScale.Fit
)

Para cargar imágenes desde URL, necesitarás la extensión de Coil para Glance (o Glide).

// Asegúrate de tener la dependencia:
// implementation("androidx.glance:glance-appwidget-extension-coil:1.0.0")

import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.appwidget.ImageProvider
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.layout.height
import androidx.glance.layout.ContentScale
import coil.compose.AsyncImagePainter
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.items
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight

class ImageWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: android.content.Context, id: androidx.glance.appwidget.AppWidgetId) {
        provideContent {
            Column(
                modifier = GlanceModifier.fillMaxSize().background(Color.White).padding(8.dp)
            ) {
                Text("Mi Widget con Imagen", style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color.Black))
                Image(
                    provider = ImageProvider(R.drawable.widget_preview), // Usando una imagen local de ejemplo
                    contentDescription = "Imagen de ejemplo",
                    modifier = GlanceModifier.width(100.dp).height(100.dp).padding(top = 8.dp),
                    contentScale = ContentScale.Fit
                )
            }
        }
    }
}
// Y su respectivo receiver y appwidget-provider si es un widget separado.

Listas Desplazables (LazyColumn)

Glance ofrece LazyColumn para mostrar listas de elementos. Es muy similar al LazyColumn de Compose UI, pero funciona con las limitaciones de RemoteViews.

package com.example.myglanceapp

import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.provideContent
import androidx.glance.GlanceModifier
import androidx.glance.background
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.dp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
import androidx.glance.layout.Alignment
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.items

// Suponiendo que tienes una lista de datos
data class TodoItem(val id: Int, val text: String, val done: Boolean)

val todoList = listOf(
    TodoItem(1, "Comprar leche", false),
    TodoItem(2, "Pasear al perro", true),
    TodoItem(3, "Estudiar Glance", false),
    TodoItem(4, "Llamar a mamá", false)
)

class ListWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: android.content.Context, id: androidx.glance.appwidget.AppWidgetId) {
        provideContent {
            Column(
                modifier = GlanceModifier
                    .fillMaxSize()
                    .background(Color(0xFFFFFDE7)) // Un amarillo claro de fondo
                    .padding(8.dp)
            ) {
                Text(
                    text = "Mis Tareas",
                    style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp, color = Color.Black),
                    modifier = GlanceModifier.padding(bottom = 8.dp)
                )
                LazyColumn(modifier = GlanceModifier.fillMaxSize()) {
                    items(todoList) {
                        Row(
                            modifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp),
                            verticalAlignment = Alignment.Vertical.CenterVertically
                        ) {
                            Text(
                                text = if (it.done) "✅ ${it.text}" else "⬜ ${it.text}",
                                style = TextStyle(fontSize = 14.sp, color = Color.DarkGray)
                            )
                        }
                    }
                }
            }
        }
    }
}

class ListWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = ListWidget()
}

Este ejemplo muestra una lista de tareas. Puedes adaptar el TodoItem y todoList a tus necesidades. Cada elemento de la lista puede tener su propia lógica de clic, etc.

🔥 Importante: Para que LazyColumn funcione correctamente con interacción (por ejemplo, clic en cada item), cada item debe tener un `GlanceModifier.clickable` que dispare una acción única, por ejemplo, usando un `ActionParameters` para identificar el item clicado.

🧩 Personalización y Reconfiguración de Widgets

Android permite que los widgets sean configurables por el usuario. Jetpack Glance simplifica este proceso.

1. Pantalla de Configuración

Para permitir que el usuario configure el widget al añadirlo, puedes especificar una configure actividad en tu appwidget-provider XML:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:configure="com.example.myglanceapp.WidgetConfigurationActivity">
</appwidget-provider>

Crea una WidgetConfigurationActivity.kt que se encargará de la configuración. Esta actividad debe devolver RESULT_OK con el appWidgetId.

package com.example.myglanceapp

import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.updateAppWidgetState
import androidx.glance.appwidget.update
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.runBlocking

val CUSTOM_TEXT_KEY = stringPreferencesKey("custom_text")

@OptIn(ExperimentalMaterial3Api::class)
class WidgetConfigurationActivity : ComponentActivity() {

    private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Extraer el appWidgetId del intent
        val extras = intent.extras
        if (extras != null) {
            appWidgetId = extras.getInt(
                AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID
            )
        }

        // Si el id es inválido, finaliza la actividad
        if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            finish()
            return
        }

        setContent {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                var customText by remember { mutableStateOf("Default Text") }

                Column(
                    modifier = Modifier.fillMaxSize().padding(16.dp),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text("Configurar Widget", style = MaterialTheme.typography.headlineMedium)
                    TextField(
                        value = customText,
                        onValueChange = { customText = it },
                        label = { Text("Texto personalizado") },
                        modifier = Modifier.padding(vertical = 16.dp)
                    )
                    Button(onClick = {
                        // Guardar la configuración en el estado del widget
                        runBlocking {
                            val glanceId = GlanceAppWidgetManager(this@WidgetConfigurationActivity).getGlanceId(appWidgetId)
                            updateAppWidgetState(this@WidgetConfigurationActivity, glanceId) { prefs ->
                                prefs[CUSTOM_TEXT_KEY] = customText
                            }
                            MyGlanceWidget().update(this@WidgetConfigurationActivity, glanceId)
                        }

                        // Devolver RESULT_OK
                        val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                        setResult(Activity.RESULT_OK, resultValue)
                        finish()
                    }) {
                        Text("Guardar y Añadir")
                    }
                }
            }
        }
    }
}

Modifica tu MyGlanceWidget para que use el texto personalizado del estado:

// ... dentro de MyGlanceWidget provideGlance
val prefs = currentState<Preferences>()
val customText = prefs[CUSTOM_TEXT_KEY] ?: "No configurado"

Text(
    text = customText,
    style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 24.sp, color = Color.Black),
    modifier = GlanceModifier.padding(bottom = 16.dp)
)
// ... resto del widget

Registra WidgetConfigurationActivity en AndroidManifest.xml:

<activity android:name=".WidgetConfigurationActivity"
    android:exported="false">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
    </intent-filter>
</activity>

Ahora, cuando el usuario intente añadir el widget, se lanzará la WidgetConfigurationActivity y podrá introducir un texto personalizado.


📊 Comparativa con RemoteViews Tradicional

CaracterísticaRemoteViews TradicionalJetpack Glance
---------
Paradigma UIImperativo (construcción manual de vistas)Declarativo (similar a Jetpack Compose)
Complejidad del códigoAlta (mucho boilerplate, manipulación de IDs)Baja (más conciso, centrado en la UI)
---------
Gestión de estadoManual, requiere PendingIntents complejosIntegrado con GlanceStateDefinition y DataStore
ReactividadLimitada, requiere actualizaciones manualesReactiva, actualizaciones automáticas al cambiar el estado
---------
Curva de aprendizajeEmpinadaMás suave para desarrolladores de Compose
Estilos y temasLimitado a XML y @styleSoporte para TextStyle, Color, GlanceModifier
---------
RendimientoBueno, pero puede ser propenso a erroresOptimizado para widgets, construido sobre RemoteViews
🔥 Importante: Jetpack Glance no reemplaza `RemoteViews`, sino que abstrae su complejidad, permitiéndote trabajar con un modelo más moderno y eficiente.

💡 Buenas Prácticas y Consejos Avanzados

  • Rendimiento: Mantén tus widgets ligeros. Evita operaciones de red o base de datos intensivas en el hilo principal. Usa WorkManager para tareas pesadas.
  • Diseño Responsivo: Considera diferentes tamaños de widget y orientaciones. Glance soporta GlanceModifier.fillMaxSize(), width(), height() con dp para flexibilidad. Las capacidades de resizeMode en appwidget-provider XML son clave.
  • Manejo de Errores: Incluye lógica para manejar errores al cargar datos o realizar acciones. Muestra mensajes útiles al usuario.
  • Accesibilidad: Proporciona contentDescription para Image y otros elementos visuales para mejorar la accesibilidad.
  • Pruebas: Prueba tus widgets en diferentes dispositivos y versiones de Android para asegurar un comportamiento consistente.
  • Seguridad: Ten cuidado con la información sensible que muestras en un widget, ya que puede ser visible para terceros.

❓ Preguntas Frecuentes (FAQ)

¿Glance reemplaza completamente los AppWidgetProvider tradicionales? No directamente. Glance se construye sobre la API de `RemoteViews` y `AppWidgetProvider`. Lo que hace es proporcionar una abstracción declarativa que simplifica enormemente la implementación. Los `GlanceAppWidgetReceiver` son de hecho una extensión de `AppWidgetProvider`.
¿Puedo usar todos los composables de Jetpack Compose en Glance? No. Glance tiene su propio conjunto de composables que son un subconjunto de los composables de Compose UI, adaptados para trabajar con las limitaciones de `RemoteViews`. Por ejemplo, no encontrarás composables interactivos complejos o animaciones avanzadas en Glance.
¿Cómo puedo depurar problemas en mi widget Glance? Utiliza el `Logcat` de Android Studio. Los errores en el renderizado del widget o en las acciones suelen aparecer allí. También puedes usar el **App Widget Host** en el emulador o dispositivo para ver el estado de tu widget.
¿Hay limitaciones en el número de widgets que se pueden añadir? No hay un límite estricto impuesto por Android, pero el rendimiento del dispositivo puede verse afectado si se añaden demasiados widgets o si son muy pesados en recursos.

Conclusión ✨

Jetpack Glance es una adición fantástica al ecosistema de desarrollo de Android, simplificando la creación de widgets de aplicación que son más interactivos y fáciles de mantener. Al adoptar el paradigma declarativo de Jetpack Compose, permite a los desarrolladores construir interfaces de usuario de widgets de manera más eficiente y con menos código, mejorando la experiencia general del usuario.

Al dominar Glance, puedes crear widgets dinámicos y atractivos que mantendrán a tus usuarios comprometidos con tu aplicación directamente desde su pantalla de inicio. ¡Anímate a experimentar y llevar la interactividad de tu app al siguiente nivel!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!