Cómo crear un Widget de Android en Flutter con home_widget: Guía completa paso a paso

- Andrés Cruz - EN In english

Cómo crear un Widget de Android en Flutter con home_widget: Guía completa paso a paso

Los widgets de pantalla de inicio en Android son una de las funcionalidades más solicitadas por los usuarios. Permiten mostrar información relevante de tu aplicación directamente en el escritorio del dispositivo, sin necesidad de abrir la app. En este artículo aprenderás cómo crear un widget nativo de Android en tu proyecto Flutter utilizando el paquete home_widget, logrando una sincronización perfecta entre los datos de tu app y lo que se muestra en el escritorio.

Vamos a construir un widget real: un contador de racha (streak) que muestra cuántos días consecutivos el usuario ha usado la aplicación. El widget se actualizará automáticamente cada vez que el valor cambie dentro de la app Flutter.

Requisitos previos

  • Un proyecto Flutter funcional con soporte para Android.
  • Conocimientos básicos de Dart, Kotlin y XML para layouts de Android.
  • Flutter SDK 3.10 o superior.
  • Android SDK compileSdk 33 o superior.

Paso 1: Instalar el paquete home_widget en Flutter

El paquete home_widget es la pieza clave que conecta tu aplicación Flutter con el sistema de widgets nativos de Android. Este paquete se encarga de almacenar datos en SharedPreferences del lado nativo y de enviar señales de actualización al widget cuando los datos cambian.

Abre tu archivo pubspec.yaml y añade la dependencia:

dependencies:
  flutter:
    sdk: flutter
  home_widget:

Luego ejecuta en tu terminal:

$ flutter pub get

Con esto, el paquete quedará instalado y listo para ser utilizado tanto en el código Dart como en el código nativo de Android.

Paso 2: Crear el diseño visual del widget (XML Layout)

Los widgets de Android utilizan un sistema de vistas remotas (RemoteViews), lo que significa que el diseño se define mediante un archivo XML tradicional de Android, no con el sistema de widgets de Flutter. Solo un subconjunto de vistas está permitido: LinearLayout, RelativeLayout, TextView, ImageView, entre otros.

2.1 Crear el archivo de layout

Crea el archivo en la siguiente ruta de tu proyecto:

android/app/src/main/res/layout/streak_widget.xml

Este archivo define la estructura visual que el usuario verá en su pantalla de inicio. A continuación, el código completo:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="12dp"
    android:id="@+id/widget_container">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        android:background="@drawable/widget_background"
        android:elevation="6dp">

        <ImageView
            android:id="@+id/widget_icon"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:src="@mipmap/launcher_icon"
            android:layout_marginBottom="8dp" />

        <TextView
            android:id="@+id/streak_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0"
            android:textColor="#FFFFFF"
            android:textSize="36sp"
            android:textStyle="bold"
            android:fontFamily="sans-serif-black"
            android:includeFontPadding="false"
            android:shadowColor="#40000000"
            android:shadowDx="1"
            android:shadowDy="2"
            android:shadowRadius="3" />

        <TextView
            android:id="@+id/streak_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="DÍAS DE RACHA"
            android:textColor="#F3E5F5"
            android:textSize="11sp"
            android:textStyle="bold"
            android:fontFamily="sans-serif-medium"
            android:letterSpacing="0.05"
            android:layout_marginTop="2dp" />
    </LinearLayout>
</RelativeLayout>

Analicemos los puntos clave de este layout:

  • @+id/widget_container: El contenedor principal del widget. Este ID lo usaremos más adelante para asignar un evento de clic que abra la aplicación.
  • @+id/streak_text: El TextView que mostrará el valor numérico de la racha. Este es el ID que actualizaremos dinámicamente desde Kotlin con el valor sincronizado desde Flutter.
  • @+id/streak_label: La etiqueta descriptiva debajo del número.
  • @drawable/widget_background: Un fondo personalizado definido como un drawable XML, que veremos en el siguiente paso.

2.2 Crear el fondo del widget (drawable)

Para que el widget tenga un aspecto profesional y atractivo, creamos un fondo con un degradado de colores y esquinas redondeadas. Crea el archivo:

android/app/src/main/res/drawable/widget_background.xml

Con el siguiente contenido:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="135"
        android:centerColor="#8E24AA"
        android:endColor="#4A148C"
        android:startColor="#AB47BC"
        android:type="linear" />
    <corners android:radius="24dp" />
</shape>

Este drawable crea un rectángulo con un degradado lineal que va de un violeta claro (#AB47BC) a un violeta oscuro (#4A148C), con esquinas redondeadas de 24dp. El ángulo de 135 grados genera un degradado diagonal que le da un aspecto moderno al widget.

Paso 3: Configurar los metadatos del widget (appwidget-provider)

Android necesita un archivo XML de metadatos que le informe al sistema operativo las características del widget: su tamaño mínimo, frecuencia de actualización, layout inicial y comportamiento de redimensionamiento.

Crea el archivo en:

android/app/src/main/res/xml/streak_widget_info.xml

Con el siguiente contenido:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="110dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:initialLayout="@layout/streak_widget"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>

Explicación de cada atributo:

  • android:minWidth y android:minHeight: Definen el tamaño mínimo del widget en el grid de la pantalla de inicio. Android traduce estos valores a celdas del grid.
  • android:updatePeriodMillis: Define cada cuántos milisegundos el sistema invocará el método onUpdate del proveedor del widget. El valor 86400000 equivale a 24 horas. Es importante saber que Android no garantiza actualizaciones más frecuentes que cada 30 minutos. Sin embargo, en nuestro caso las actualizaciones principales vendrán programáticamente desde Flutter a través de home_widget, no de este temporizador.
  • android:initialLayout: Referencia al archivo de layout que creamos en el Paso 2. Este es el diseño que Android usará para renderizar el widget.
  • android:resizeMode: Permite al usuario redimensionar el widget horizontal y verticalmente.
  • android:widgetCategory: Indica que este widget es para la pantalla de inicio (home_screen).

Paso 4: Crear el AppWidgetProvider en Kotlin

El AppWidgetProvider es la clase nativa de Android que se encarga de recibir los eventos de actualización del widget y de renderizar los datos en la vista. Aquí es donde ocurre la lectura de los datos sincronizados desde Flutter.

Crea el archivo en la ruta correspondiente a tu paquete de Kotlin:

android/app/src/main/kotlin/tu/paquete/app/StreakWidgetProvider.kt

El código completo:

package tu.paquete.app

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetLaunchIntent
import es.antonborri.home_widget.HomeWidgetPlugin

class StreakWidgetProvider : AppWidgetProvider() {
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        for (appWidgetId in appWidgetIds) {
            val views = RemoteViews(context.packageName, R.layout.streak_widget)

            // Obtener los datos sincronizados desde Flutter via home_widget
            val widgetData = HomeWidgetPlugin.getData(context)
            val streakValue = widgetData.getInt("streak", 0)

            // Actualizar el TextView con el valor de la racha
            views.setTextViewText(R.id.streak_text, streakValue.toString())

            // Crear un Intent para abrir la app al presionar el widget
            val pendingIntent = HomeWidgetLaunchIntent.getActivity(
                context,
                MainActivity::class.java
            )
            views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)

            // Aplicar los cambios al widget
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

Desglosemos las partes más importantes de esta clase:

Lectura de datos con HomeWidgetPlugin.getData()

La línea más crítica de todo el archivo es:

val widgetData = HomeWidgetPlugin.getData(context)
val streakValue = widgetData.getInt("streak", 0)

El método HomeWidgetPlugin.getData(context) devuelve las SharedPreferences que el paquete home_widget utiliza internamente para almacenar los datos enviados desde Flutter. Esto es esencial: no debes usar context.getSharedPreferences("nombre_custom", ...) para leer los datos, ya que los datos guardados desde Flutter con HomeWidget.saveWidgetData() se almacenan en un archivo de preferencias específico del paquete, no en uno con nombre arbitrario.

Si utilizas un nombre incorrecto de SharedPreferences, el widget siempre mostrará el valor por defecto (en este caso, 0), ya que estará leyendo de un archivo vacío.

Apertura de la app al hacer clic

HomeWidgetLaunchIntent.getActivity() es una utilidad del paquete home_widget que genera un PendingIntent correctamente configurado para abrir tu MainActivity de Flutter cuando el usuario toca el widget. Luego asignamos este intent al contenedor principal del widget con setOnClickPendingIntent().

Bucle de actualización

El bucle for (appWidgetId in appWidgetIds) es necesario porque el usuario puede tener múltiples instancias del mismo widget en su pantalla de inicio. Cada una debe actualizarse individualmente.

Paso 5: Registrar el widget en el AndroidManifest.xml

Para que Android reconozca tu widget, debes declararlo como un receiver dentro de la etiqueta <application> del archivo AndroidManifest.xml:

android/app/src/main/AndroidManifest.xml

Añade el siguiente bloque dentro de <application>, después de los meta-data existentes:

<receiver android:name=".StreakWidgetProvider" android:exported="true">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/streak_widget_info" />
</receiver>

Puntos clave del registro:

  • android:name=".StreakWidgetProvider": El nombre de la clase Kotlin que creamos. El punto inicial indica que está en el paquete raíz declarado en el namespace.
  • android:exported="true": Necesario para que el sistema de widgets de Android pueda comunicarse con tu receiver.
  • APPWIDGET_UPDATE: Es el intent que el sistema envía cuando necesita actualizar el widget.
  • @xml/streak_widget_info: Referencia al archivo de metadatos que creamos en el Paso 3.

Paso 6: Sincronizar datos desde Flutter con home_widget

Este es el paso donde todo cobra vida. Desde el código Dart de tu aplicación Flutter, enviarás los datos al widget nativo cada vez que cambien. El paquete home_widget se encarga de escribir los datos en las SharedPreferences nativas y de enviar un broadcast al sistema Android para que actualice el widget.

6.1 Crear el Provider en Flutter

En este ejemplo utilizamos un ChangeNotifier de Provider para gestionar el estado de la racha y sincronizar automáticamente con el widget:

import 'package:flutter/foundation.dart';
import 'package:home_widget/home_widget.dart';

class StreakProvider with ChangeNotifier {
  int _streak = 0;
  int get streak => _streak;

  StreakProvider() {
    loadStreak();
  }

  Future<void> loadStreak() async {
    // Aquí obtienes el valor de tu fuente de datos (base de datos, API, etc.)
    _streak = await obtenerStreakDesdeTuFuenteDeDatos();
    await _updateWidget();
    notifyListeners();
  }

  Future<void> incrementStreak() async {
    // Tu lógica de negocio para incrementar la racha
    await guardarNuevoStreak();
    await loadStreak();
  }

  Future<void> _updateWidget() async {
    try {
      // 1. Guardar el dato en SharedPreferences nativas
      await HomeWidget.saveWidgetData<int>('streak', _streak);

      // 2. Notificar al sistema Android para que actualice el widget
      await HomeWidget.updateWidget(
        name: 'StreakWidgetProvider',
        androidName: 'StreakWidgetProvider',
      );
    } catch (e) {
      debugPrint('Error updating widget: $e');
    }
  }
}

6.2 Explicación del flujo de sincronización

El método _updateWidget() realiza dos operaciones fundamentales:

  1. HomeWidget.saveWidgetData<int>('streak', _streak): Escribe el valor de la racha en las SharedPreferences nativas de Android bajo la clave 'streak'. Esta es la misma clave que luego leeremos desde Kotlin con widgetData.getInt("streak", 0). Es crucial que ambas claves coincidan exactamente.
  2. HomeWidget.updateWidget(name: 'StreakWidgetProvider', androidName: 'StreakWidgetProvider'): Envía un broadcast APPWIDGET_UPDATE al sistema Android, lo que provoca que se ejecute el método onUpdate() de tu StreakWidgetProvider. El parámetro androidName debe coincidir exactamente con el nombre de tu clase Kotlin.

Es decir, cada vez que llamas a _updateWidget():

  1. Flutter escribe el dato en el almacenamiento nativo.
  2. Flutter le dice a Android: "Oye, actualiza este widget".
  3. Android ejecuta onUpdate() en tu StreakWidgetProvider.
  4. El Provider de Kotlin lee el dato de SharedPreferences y actualiza la vista.

Paso 7: Registrar el Provider en tu aplicación

En tu archivo main.dart, registra el StreakProvider para que esté disponible en todo el árbol de widgets de Flutter:

import 'package:provider/provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => StreakProvider()),
        // ... tus otros providers
      ],
      child: const MyApp(),
    ),
  );
}

De esta forma, cada vez que el StreakProvider se inicialice (al abrir la app), cargará el valor actualizado de la racha y lo sincronizará con el widget del escritorio.

Resumen de la estructura de archivos

Para tener una visión clara de todos los archivos involucrados, esta es la estructura completa:

tu_proyecto/
├── lib/
│   └── providers/
│       └── streak_provider.dart          ← Provider Flutter (envía datos)
├── android/
│   └── app/
│       └── src/
│           └── main/
│               ├── AndroidManifest.xml    ← Registro del receiver
│               ├── kotlin/tu/paquete/app/
│               │   ├── MainActivity.kt
│               │   └── StreakWidgetProvider.kt  ← Provider nativo (lee datos)
│               └── res/
│                   ├── drawable/
│                   │   └── widget_background.xml  ← Fondo del widget
│                   ├── layout/
│                   │   └── streak_widget.xml      ← Layout visual del widget
│                   └── xml/
│                       └── streak_widget_info.xml ← Metadatos del widget
└── pubspec.yaml                           ← Dependencia home_widget

Errores comunes y cómo solucionarlos

Error: El widget siempre muestra el valor por defecto (0)

Este es el error más frecuente. Ocurre cuando el lado nativo (Kotlin) lee los datos de un archivo de SharedPreferences diferente al que home_widget utiliza para guardarlos. La solución es usar siempre HomeWidgetPlugin.getData(context) en lugar de context.getSharedPreferences("nombre_custom", Context.MODE_PRIVATE).

// ❌ INCORRECTO: Lee de un archivo de preferencias equivocado
val prefs = context.getSharedPreferences("HomeWidgetPreferences", Context.MODE_PRIVATE)
val streakValue = prefs.getInt("streak", 0)

// ✅ CORRECTO: Lee del archivo de preferencias del paquete home_widget
val widgetData = HomeWidgetPlugin.getData(context)
val streakValue = widgetData.getInt("streak", 0)

Error: El nombre del androidName no coincide

El parámetro androidName en HomeWidget.updateWidget() debe ser exactamente el nombre de la clase Kotlin, sin el paquete completo. Si tu clase se llama StreakWidgetProvider, entonces:

// ✅ Correcto
await HomeWidget.updateWidget(
  name: 'StreakWidgetProvider',
  androidName: 'StreakWidgetProvider',
);

// ❌ Incorrecto
await HomeWidget.updateWidget(
  name: 'streak_widget',
  androidName: 'net.desarrollolibre.practicaingles.StreakWidgetProvider',
);

Error: Las claves no coinciden entre Dart y Kotlin

La clave usada en HomeWidget.saveWidgetData<int>('streak', _streak) de Dart debe ser idéntica a la usada en widgetData.getInt("streak", 0) en Kotlin. Una simple diferencia de mayúsculas o un espacio extra causará que el widget no encuentre el dato.

Error de compilación: JVM target incompatible

Si al compilar recibes un error como "Cannot inline bytecode built with JVM target 11 into bytecode that is being built with JVM target 1.8", significa que los subproyectos de plugins no comparten la misma versión de JVM que tu app. Puedes solucionarlo configurando el build.gradle.kts raíz del proyecto Android para forzar la versión de JVM en todos los subproyectos.

Consejos para mejorar tu widget

  • Actualización inmediata: Llama a _updateWidget() cada vez que el dato relevante cambie, no solo al iniciar la app. Así el widget se mantiene siempre actualizado.
  • Manejo de errores: Envuelve la lógica de actualización del widget en un bloque try-catch. Un fallo al actualizar el widget no debería impedir el funcionamiento normal de tu app.
  • Diseño visual: Utiliza drawables con degradados y esquinas redondeadas para que tu widget se integre de forma elegante con la estética del sistema operativo. Un buen diseño aumenta significativamente la probabilidad de que los usuarios mantengan el widget en su pantalla de inicio.
  • Icono de la app: Incluye el icono de tu aplicación en el widget usando @mipmap/launcher_icon para reforzar el branding y que el usuario identifique fácilmente a qué app pertenece el widget.
  • Interacción al tocar: Siempre configura un intent para que al presionar el widget se abra tu aplicación. Esto mejora la experiencia de usuario y facilita la navegación.

Conclusión

Crear un widget de Android en Flutter con el paquete home_widget requiere trabajar con archivos nativos de Android (XML y Kotlin), pero el proceso es bastante directo una vez que comprendes la arquitectura. La clave está en entender que:

  1. El layout XML define cómo se ve el widget.
  2. El appwidget-provider XML define las propiedades del widget para el sistema.
  3. El AppWidgetProvider en Kotlin lee los datos y actualiza la vista.
  4. El código Dart escribe los datos y solicita la actualización.
  5. El AndroidManifest.xml registra todo en el sistema Android.

El paquete home_widget actúa como el puente entre Flutter y el sistema nativo de widgets de Android, abstrayendo la complejidad de las SharedPreferences y los broadcasts del sistema. Siguiendo los pasos de esta guía, puedes implementar cualquier tipo de widget que muestre datos de tu aplicación Flutter en la pantalla de inicio del dispositivo Android.

Aprende paso a paso cómo crear un widget de Android en Flutter usando el paquete home_widget. Guía completa con código en Kotlin, XML y Dart para sincronizar datos entre tu app Flutter y un widget nativo de la pantalla de inicio de Android.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.