¡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.
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.
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")
}
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:
MyGlanceWidgetextiendeGlanceAppWidget. La funciónprovideGlancees donde defines la UI de tu widget usando composables de Glance.provideContentes una función de extensión paraGlanceAppWidgetque toma un bloqueComposabledonde defines el diseño del widget. Aquí usamosColumnyText, que son similares a sus contrapartes en Jetpack Compose.MyGlanceWidgetReceiverextiendeGlanceAppWidgetReceiver. Esta es la clase que el sistema Android instanciará. Necesita saber quéGlanceAppWidgetdebe mostrar, por eso sobrescribimosglanceAppWidget.
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>
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.
🔄 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.
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.
🧩 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ística | RemoteViews Tradicional | Jetpack Glance |
|---|---|---|
| --- | --- | --- |
| Paradigma UI | Imperativo (construcción manual de vistas) | Declarativo (similar a Jetpack Compose) |
| Complejidad del código | Alta (mucho boilerplate, manipulación de IDs) | Baja (más conciso, centrado en la UI) |
| --- | --- | --- |
| Gestión de estado | Manual, requiere PendingIntents complejos | Integrado con GlanceStateDefinition y DataStore |
| Reactividad | Limitada, requiere actualizaciones manuales | Reactiva, actualizaciones automáticas al cambiar el estado |
| --- | --- | --- |
| Curva de aprendizaje | Empinada | Más suave para desarrolladores de Compose |
| Estilos y temas | Limitado a XML y @style | Soporte para TextStyle, Color, GlanceModifier |
| --- | --- | --- |
| Rendimiento | Bueno, pero puede ser propenso a errores | Optimizado para widgets, construido sobre RemoteViews |
💡 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
WorkManagerpara tareas pesadas. - Diseño Responsivo: Considera diferentes tamaños de widget y orientaciones. Glance soporta
GlanceModifier.fillMaxSize(),width(),height()condppara flexibilidad. Las capacidades deresizeModeenappwidget-providerXML son clave. - Manejo de Errores: Incluye lógica para manejar errores al cargar datos o realizar acciones. Muestra mensajes útiles al usuario.
- Accesibilidad: Proporciona
contentDescriptionparaImagey 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
- ¡Poder sin límites! Integrando APIs de Hardware Android: Sensores, NFC y Bluetooth LEintermediate20 min
- ¡Poder y Flexibilidad! Construyendo Componentes Reutilizables en Android con Jetpack Compose y Modificadores Personalizadosintermediate25 min
- ¡Despide la Fragmentación! Construyendo una Arquitectura Modular Robusta en Android con Dynamic Feature Modulesintermediate20 min
- Carga de Datos Offline en Android: Implementando una Estrategia Robusta con Room y WorkManagerintermediate25 min
- Navegación Avanzada en Android: Jetpack Navigation Component y Deep Linksintermediate25 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!