Cómo crear un widget avanzado

En esta página, se explican las prácticas recomendadas para crear un widget más avanzado y así mejorar la experiencia del usuario.

Optimizaciones para actualizar el contenido de widgets

La actualización del contenido del widget puede ser costosa en términos de procesamiento. Para ahorrar el consumo de batería, optimiza el tipo de actualización, la frecuencia y el tiempo.

Tipos de actualizaciones de widgets

Existen tres maneras de actualizar un widget: una actualización completa, una actualización parcial y, en el caso de un widget de colección, una actualización de datos. Cada uno tiene diferentes costos computacionales y ramificaciones.

A continuación, se describe cada tipo de actualización y se proporcionan fragmentos de código para cada uno.

  • Actualización completa: Llama a AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) para actualizar el widget por completo. Esto reemplaza el RemoteViews proporcionado anteriormente por un RemoteViews nuevo. Esta es la actualización más costosa en términos de procesamiento.

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout1, "Updated text1")
    setTextViewText(R.id.textview_widget_layout2, "Updated text2")
    }
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
    

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout1, "Updated text1");
    remoteViews.setTextViewText(R.id.textview_widget_layout2, "Updated text2");
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    
  • Actualización parcial: Llama a AppWidgetManager.partiallyUpdateAppWidget para actualizar partes del widget. Esto combina el RemoteViews nuevo con el RemoteViews proporcionado antes. Se ignora este método si un widget no recibe al menos una actualización completa a través de updateAppWidget(int[], RemoteViews).

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout, "Updated text")
    }
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews)
    

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout, "Updated text");
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews);
    
  • Actualización de los datos de la colección: Llama a AppWidgetManager.notifyAppWidgetViewDataChanged para invalidar los datos de una vista de colección en tu widget. Esto activa RemoteViewsFactory.onDataSetChanged. Mientras tanto, los datos antiguos se muestran en el widget. Con este método, puedes realizar tareas costosas de forma segura y síncrona.

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)
    
    

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview);
    

Puedes llamar a estos métodos desde cualquier parte de tu app, siempre que esta tenga el mismo UID que la clase AppWidgetProvider correspondiente.

Cómo determinar la frecuencia con la que debes actualizar un widget

Los widgets se actualizan periódicamente según el valor proporcionado para el atributo updatePeriodMillis. El widget se puede actualizar en respuesta a la interacción del usuario, a las actualizaciones de transmisión o a ambas.

Actualizar periódicamente

Puedes controlar la frecuencia de la actualización periódica especificando un valor para AppWidgetProviderInfo.updatePeriodMillis en el XML appwidget-provider. Cada actualización activa el método AppWidgetProvider.onUpdate(), que es donde puedes colocar el código para actualizar el widget. Sin embargo, ten en cuenta las alternativas para las actualizaciones del receptor de emisión que se describen en la siguiente sección si el widget necesita cargar datos de forma asíncrona o tarda más de 10 segundos en actualizarse, ya que después de 10 segundos, el sistema considera que un BroadcastReceiver no responde.

updatePeriodMillis no admite valores de menos de 30 minutos. Sin embargo, si deseas inhabilitar las actualizaciones periódicas, puedes especificar 0.

Puedes permitir que los usuarios ajusten la frecuencia de las actualizaciones en una configuración. Por ejemplo, es posible que quieran que un código bursátil se actualice cada 15 minutos o solo cuatro veces al día. En este caso, establece updatePeriodMillis en 0 y usa WorkManager en su lugar.

Cómo actualizar en respuesta a una interacción del usuario

Estas son algunas formas recomendadas de actualizar el widget según la interacción del usuario:

  • Desde una actividad de la app: Llama directamente a AppWidgetManager.updateAppWidget en respuesta a una interacción del usuario, como su presión.

  • Desde interacciones remotas, como una notificación o el widget de una app: Construye un PendingIntent y, luego, actualiza el widget desde Activity, Broadcast o Service invocados. Puedes elegir tu propia prioridad. Por ejemplo, si seleccionas un Broadcast para el PendingIntent, puedes elegir una transmisión en primer plano para dar prioridad a BroadcastReceiver.

Cómo actualizar en respuesta a un evento de emisión

Un ejemplo de un evento de emisión que requiere que se actualice un widget es cuando el usuario toma una foto. En este caso, te recomendamos que actualices el widget cuando se detecte una foto nueva.

Puedes programar un trabajo con JobScheduler y especificar una transmisión como activador mediante el método JobInfo.Builder.addTriggerContentUri.

También puedes registrar un BroadcastReceiver para la transmisión, por ejemplo, si escuchas ACTION_LOCALE_CHANGED. Sin embargo, debido a que consume recursos del dispositivo, hazlo con cuidado y escucha solo la transmisión específica. Con la introducción de las limitaciones de transmisión en Android 7.0 (nivel de API 24) y Android 8.0 (nivel de API 26), las apps no pueden registrar transmisiones implícitas en sus manifiestos, con algunas excepciones.

Consideraciones para la actualización de un widget desde un BroadcastReceiver

Si el widget se actualiza desde un BroadcastReceiver, incluido AppWidgetProvider, ten en cuenta las siguientes consideraciones sobre la duración y la prioridad de una actualización de widget.

Duración de la actualización

Como regla, el sistema permite que los receptores de emisión, que generalmente se ejecutan en el subproceso principal de la app, se ejecuten durante 10 segundos antes de considerarlos no responsivos y activen un error de Aplicación no responde (ANR). Si la actualización del widget tarda más tiempo, considera las siguientes alternativas:

  • Programa una tarea con WorkManager.

  • Dale más tiempo al receptor con el método goAsync. Esto permite que los receptores se ejecuten durante 30 segundos.

Consulta Consideraciones de seguridad y prácticas recomendadas para obtener más información.

Prioridad de la actualización

De forma predeterminada, las transmisiones, incluidas las que se realizan con AppWidgetProvider.onUpdate, se ejecutan como procesos en segundo plano. Esto significa que los recursos del sistema sobrecargados pueden causar un retraso en la invocación del receptor de emisión. Para priorizar la transmisión, conviértela en un proceso en primer plano.

Por ejemplo, agrega la marca Intent.FLAG_RECEIVER_FOREGROUND al Intent que se pasa a PendingIntent.getBroadcast cuando el usuario presiona una parte determinada del widget.

Crear vistas previas precisas que incluyan elementos dinámicos

Figura 1: Vista previa de un widget que no muestra elementos de lista.

En esta sección, se explica el enfoque recomendado para mostrar varios elementos en una vista previa de un widget con una vista de colección, es decir, un widget que use ListView, GridView o StackView.

Si tu widget usa una de estas vistas, crear una vista previa escalable proporcionando directamente el diseño real del widget degrada la experiencia cuando la vista previa del widget no muestra ningún elemento. Esto ocurre porque los datos de la vista de colección se configuran de forma dinámica durante el tiempo de ejecución y se ven similares a la imagen que se muestra en la figura 1.

Para que las vistas previas de los widgets con vistas de colecciones se muestren correctamente en el selector de widgets, recomendamos mantener un archivo de diseño separado designado solo para la vista previa. Este archivo de diseño independiente incluye el diseño real del widget y una vista de colección del marcador de posición con elementos falsos. Por ejemplo, para imitar un ListView, proporciona un marcador de posición LinearLayout con varios elementos de lista falsos.

Para ilustrar un ejemplo de un objeto ListView, comienza con un archivo de diseño independiente:

// res/layout/widget_preview.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:background="@drawable/widget_background"
   android:orientation="vertical">

    // Include the actual widget layout that contains ListView.
    <include
        layout="@layout/widget_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    // The number of fake items you include depends on the values you provide
    // for minHeight or targetCellHeight in the AppWidgetProviderInfo
    // definition.

    <TextView android:text="@string/fake_item1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

    <TextView android:text="@string/fake_item2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

</LinearLayout>

Especifica el archivo de diseño de vista previa cuando proporciones el atributo previewLayout de los metadatos AppWidgetProviderInfo. Aún debes especificar el diseño real del widget para el atributo initialLayout y usar el diseño real del widget cuando se construye un RemoteViews en el tiempo de ejecución.

<appwidget-provider
    previewLayout="@layout/widget_previe"
    initialLayout="@layout/widget_view" />

Elementos de lista complejos

En el ejemplo de la sección anterior, se proporcionan elementos de lista falsos, ya que estos son objetos TextView. Puede ser más complejo proporcionar elementos falsos si estos son diseños complejos.

Considera un elemento de lista que se define en widget_list_item.xml y que consta de dos objetos TextView:

<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <TextView android:id="@id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_title" />

    <TextView android:id="@id/content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_content" />
</LinearLayout>

Para proporcionar elementos de lista falsos, puedes incluir el diseño varias veces, pero esto hará que cada uno de ellos sea idéntico. Para proporcionar elementos de lista únicos, sigue estos pasos:

  1. Crea un conjunto de atributos para los valores de texto:

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. Usa estos atributos para configurar el texto:

    <LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
        <TextView android:id="@id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetTitle" />
    
        <TextView android:id="@id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetContent" />
    </LinearLayout>
    
  3. Crea tantos diseños como sea necesario para la vista previa. Redefine los valores de cada estilo:

    <resources>
    
        <style name="Theme.Widget.ListItem">
            <item name="widgetTitle"></item>
            <item name="widgetContent"></item>
        </style>
        <style name="Theme.Widget.ListItem.Preview1">
            <item name="widgetTitle">Fake Title 1</item>
            <item name="widgetContent">Fake content 1</item>
        </style>
        <style name="Theme.Widget.ListItem.Preview2">
            <item name="widgetTitle">Fake title 2</item>
            <item name="widgetContent">Fake content 2</item>
        </style>
    
    </resources>
    
  4. Aplica los estilos a los elementos falsos en el diseño de vista previa:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" ...>
    
        <include layout="@layout/widget_view" ... />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview1" />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview2" />
    
    </LinearLayout>