Créer un widget avancé

Cette page explique les bonnes pratiques à suivre pour créer un widget plus avancé afin d'améliorer l'expérience utilisateur.

Optimisations pour la mise à jour du contenu des widgets

La mise à jour du contenu des widgets peut s'avérer coûteuse en ressources de calcul. Pour économiser la batterie, optimisez le type, la fréquence et le calendrier des mises à jour.

Types de mises à jour de widget

Il existe trois façons de mettre à jour un widget: une mise à jour complète, une mise à jour partielle et, dans le cas d'un widget de collection, une actualisation des données. Chacun présente des coûts et des conséquences de calcul différents.

Vous trouverez ci-dessous une description de chaque type de mise à jour et des extraits de code correspondants.

  • Mise à jour complète:appelez AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) pour mettre à jour complètement le widget. Cette action remplace l'RemoteViews précédemment fournie par une nouvelle RemoteViews. Il s'agit de la mise à jour la plus coûteuse en termes de calcul.

    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);
    
  • Mise à jour partielle:appelez AppWidgetManager.partiallyUpdateAppWidget pour mettre à jour des parties du widget. Cette opération fusionne la nouvelle RemoteViews avec la RemoteViews précédemment fournie. Cette méthode est ignorée si un widget ne reçoit pas au moins une mise à jour complète via 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);
    
  • Actualisation des données de collection:appelez AppWidgetManager.notifyAppWidgetViewDataChanged pour invalider les données d'une vue de collection dans votre widget. Cela déclenche RemoteViewsFactory.onDataSetChanged. En attendant, les anciennes données s'affichent dans le widget. Avec cette méthode, vous pouvez effectuer des tâches coûteuses de manière synchrone en toute sécurité.

    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);
    

Vous pouvez appeler ces méthodes depuis n'importe quelle partie de votre application, à condition que l'application ait le même UID que la classe AppWidgetProvider correspondante.

Déterminer la fréquence de mise à jour d'un widget

Les widgets sont mis à jour régulièrement en fonction de la valeur fournie pour l'attribut updatePeriodMillis. Le widget peut se mettre à jour en réponse à une interaction de l'utilisateur, à des mises à jour diffusées ou aux deux.

Mettre à jour régulièrement

Vous pouvez contrôler la fréquence de la mise à jour périodique en spécifiant une valeur pour AppWidgetProviderInfo.updatePeriodMillis dans le fichier XML appwidget-provider. Chaque mise à jour déclenche la méthode AppWidgetProvider.onUpdate(), où vous pouvez placer le code pour mettre à jour le widget. Toutefois, si votre widget doit charger des données de manière asynchrone ou si sa mise à jour prend plus de 10 secondes, envisagez les alternatives aux mises à jour du broadcast receiver décrites dans la section suivante. En effet, au bout de 10 secondes, le système considère qu'un BroadcastReceiver ne répond plus.

updatePeriodMillis n'accepte pas les valeurs inférieures à 30 minutes. Toutefois, si vous souhaitez désactiver les mises à jour périodiques, vous pouvez spécifier 0.

Vous pouvez autoriser les utilisateurs à ajuster la fréquence des mises à jour dans une configuration. Par exemple, il peut souhaiter que la cotation d'une action soit actualisée toutes les 15 minutes ou seulement quatre fois par jour. Dans ce cas, définissez updatePeriodMillis sur 0 et utilisez WorkManager à la place.

Mise à jour en réponse à une interaction utilisateur

Voici quelques méthodes recommandées pour mettre à jour le widget en fonction de l'interaction de l'utilisateur:

  • À partir d'une activité de l'application:appelez directement AppWidgetManager.updateAppWidget en réponse à une interaction de l'utilisateur, comme un appui.

  • À partir d'interactions à distance, telles qu'une notification ou un widget d'application:créez un PendingIntent, puis mettez à jour le widget à partir de l'Activity, de l'Broadcast ou de l'Service appelé. Vous pouvez choisir votre propre priorité. Par exemple, si vous sélectionnez un Broadcast pour le PendingIntent, vous pouvez choisir une diffusion de premier plan pour donner la priorité au BroadcastReceiver.

Mettre à jour en réponse à un événement de diffusion

Un exemple d'événement de diffusion nécessitant la mise à jour d'un widget est lorsque l'utilisateur prend une photo. Dans ce cas, vous souhaitez mettre à jour le widget lorsqu'une nouvelle photo est détectée.

Vous pouvez planifier une tâche avec JobScheduler et spécifier une diffusion comme déclencheur à l'aide de la méthode JobInfo.Builder.addTriggerContentUri.

Vous pouvez également enregistrer un BroadcastReceiver pour la diffusion (par exemple, en écoutant ACTION_LOCALE_CHANGED). Toutefois, comme cela consomme des ressources de l'appareil, utilisez cette fonctionnalité avec précaution et n'écoutez que la diffusion spécifique. Avec l'introduction de limites de diffusion dans Android 7.0 (niveau d'API 24) et Android 8.0 (niveau d'API 26), les applications ne peuvent pas enregistrer de diffusions implicites dans leurs fichiers manifestes, à quelques exceptions près.

Éléments à prendre en compte lors de la mise à jour d'un widget à partir d'un BroadcastReceiver

Si le widget est mis à jour à partir d'un BroadcastReceiver, y compris AppWidgetProvider, tenez compte des considérations suivantes concernant la durée et la priorité d'une mise à jour de widget.

Durée de la mise à jour

En règle générale, le système laisse les broadcast receivers, qui s'exécutent généralement dans le thread principal de l'application, s'exécuter pendant 10 secondes maximum avant de les considérer comme non réactifs et de déclencher une erreur Application Not Responding (ANR, "L'application ne répond pas"). Si la mise à jour du widget prend plus de temps, envisagez les solutions suivantes:

  • Planifiez une tâche à l'aide de WorkManager.

  • Accordez plus de temps au destinataire avec la méthode goAsync. Les récepteurs peuvent ainsi s'exécuter pendant 30 secondes.

Pour en savoir plus, consultez la section Considérations et bonnes pratiques de sécurité.

Priorité de la mise à jour

Par défaut, les diffusions, y compris celles effectuées à l'aide de AppWidgetProvider.onUpdate, s'exécutent en tant que processus en arrière-plan. Cela signifie que les ressources système surchargées peuvent entraîner un retard dans l'appel du broadcast receiver. Pour donner la priorité à la diffusion, faites-en un processus de premier plan.

Par exemple, ajoutez l'indicateur Intent.FLAG_RECEIVER_FOREGROUND au Intent transmis au PendingIntent.getBroadcast lorsque l'utilisateur appuie sur une certaine partie du widget.

Créer des aperçus précis incluant des éléments dynamiques

Figure 1 : Aperçu d'un widget n'affichant aucun élément de liste.

Cette section explique l'approche recommandée pour afficher plusieurs éléments dans un aperçu de widget pour un widget avec une vue de collection, c'est-à-dire un widget qui utilise ListView, GridView ou StackView.

Si votre widget utilise l'une de ces vues, la création d'un aperçu évolutif en fournissant directement la mise en page du widget dégrade l'expérience lorsque l'aperçu du widget n'affiche aucun élément. Cela se produit parce que les données de la vue de collection sont définies de manière dynamique au moment de l'exécution. Elles ressemblent à l'image illustrée dans la figure 1.

Pour que les aperçus des widgets avec des vues de collection s'affichent correctement dans le sélecteur de widgets, nous vous recommandons de gérer un fichier de mise en page distinct réservé uniquement à l'aperçu. Ce fichier de mise en page distinct inclut la mise en page du widget et une vue de collection d'espace réservé avec de faux éléments. Par exemple, vous pouvez imiter un ListView en fournissant un espace réservé LinearLayout avec plusieurs faux éléments de liste.

Pour illustrer un exemple de ListView, commencez par un fichier de mise en page distinct:

// 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>

Spécifiez le fichier de mise en page d'aperçu lorsque vous fournissez l'attribut previewLayout des métadonnées AppWidgetProviderInfo. Vous spécifiez toujours la mise en page réelle du widget pour l'attribut initialLayout et utilisez la mise en page réelle du widget lors de la création d'un RemoteViews au moment de l'exécution.

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

Éléments de liste complexes

L'exemple de la section précédente fournit de faux éléments de liste, car les éléments de liste sont des objets TextView. Il peut être plus complexe de fournir de faux éléments si les éléments sont des mises en page complexes.

Prenons l'exemple d'un élément de liste défini dans widget_list_item.xml et composé de deux objets 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>

Pour fournir de faux éléments de liste, vous pouvez inclure la mise en page plusieurs fois, mais chaque élément de liste sera alors identique. Pour fournir des éléments de liste uniques, procédez comme suit:

  1. Créez un ensemble d'attributs pour les valeurs textuelles:

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. Utilisez ces attributs pour définir le texte:

    <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. Créez autant de styles que nécessaire pour l'aperçu. Redéfinissez les valeurs de chaque style:

    <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. Appliquez les styles aux faux éléments dans la mise en page d'aperçu:

    <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>