Créer un widget pour Android : exemples et bonnes pratiques

192/365 - Help, I'm Alive, My Heart Keeps Beating Like A Hammer

Maintenant que nous avons un aperçu des concepts de base d'une application Android, nous allons nous lancer, et commencer par créer un widget.

Mais tout d'abord, qu'est-ce qu'un widget ? C'est un concept qui, il me semble (mais je n'en suis pas du tout sûr, ne me tapez pas si je me trompe) n'existe pas sur iPhone. Il s'agit d'une mini-application installable directement sur l'accueil du téléphone, et qui permet d'afficher des données et d'interagir avec l'utilisateur.

Les widgets sont à mon avis un des points forts d'Android : ils permettent d'avoir directement sous les yeux les données les plus importantes. Idéal pour afficher la météo, un calendrier ou une todo-liste, par exemple.

Attention, la documentation parle d'«App Widgets», pour éviter la confusion avec les traditionnels widgets de formulaires. Si on me demande mon avis, je trouve le nom particulièrement mal choisi. Mais personne ne me demande mon avis, alors nous ferons avec.

Avant de commencer, voici quelques bonnes pratiques que tout développeur de widget devrait suivre. Sinon quoi ? Sinon, vous allez énerver vos utilisateurs, qui auront vite fait de renvoyer votre widget dans les limbes.

  • Allez à l'essentiel : le but d'un widget n'est pas de remplacer une application complète. Il ne faut donc pas tenter de faire tenir toute son application dans un minuscule carré, mais plutôt fournir un raccourci vers les données / actions les plus importantes. Ce qui nous amène au point suivant :

  • Créez des widgets le plus petit possible : l'espace disponible sur la home est limité. Soyez respectueux, vous n'êtes pas tout seul. Si votre widget prend inutilement trop de place, il ne tardera pas à dégager.

  • Mollo sur la batterie : quand vous développez pour les mobiles, vos ressources sont trés limitées. Et c'est particulièrement vrai pour la durée de vie de la batterie. Soyez sympa avec vos utilisateurs, évitez de créer des widgets qui se mettent à jour trop souvent.

    Idéalement, un widget ne devrait pas se mettre à jour plus d'une fois toutes les 24h. Vous trouvez que ce n'est pas assez ? Offrez au moins à l'utilisateur la possibilité de configurer cette valeur. Mais une fois tous les quarts d'heures, c'est vraiment le maximum.

    Vous pensez que votre widget devrait se mettre à jour en temps réel ? U R DOING IT WRONG ! Ne cherchez pas, vous faites fausse route.

  • Travaillez l'aspect graphique : Google a émis des recommandations de design pour les widgets. Je sais que c'est trés personnel, mais je les trouve tout de même assez… repoussantes. J'attends avec impatience que Google fasse son boulot, et remédie à la situation. Si en plus on pouvait avoir le pack graphique en format gimp / svg, ce serait pas mal aussi, merci.

  • Ne créez pas un widget quand un lanceur suffit : je vois beaucoup d'applications / développeurs qui essayent d'émuler le fonctionnement d'un lanceur via un widget. C'est mal. Google recommande de ne pas le faire, d'abord parce que ça ne sert à rien, ensuite parce que l'aspect graphique des lanceurs n'est pas fixé. Le rendu de votre widget, super sur un beau HTC avec Sense, sera peut-être horrible chez un autre constructeur, ou avec une autre version d'Android.

Les mains dans le cambouis : déclarer son widget

Avant quoi que ce soit, nous allons déclarer notre widget dans le manifest du projet. Dans le fichier Manifest.xml :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="fr.miximum.widget" android:versionCode="1"
    android:versionName="1.0">
    <uses-sdk android:minSdkVersion="8" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">

        <!-- Napply widget -->
        <receiver android:name="NapplyWidget">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data android:name="android.appwidget.provider"
                android:resource="@xml/napply_widget_meta" />
        </receiver>

        <!-- Configure activity -->
        <activity android:name="WidgetConfigure" android:theme="@android:style/Theme.Dialog">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Nous déclarons ici deux éléments. D'abord, le widget en lui-même. Ensuite, l'activity qui sera utilisée pour configurer notre widget à la création. Cette activity est facultative, mais nous allons en avoir besoin.

Notez que notre widget est déclaré via un tag «receiver», car les widget sont des broadcast receivers. En même temps, nous décrivons les types d'intents que le widget pourra recevoir. L'action « APPWIDGET_UPDATE » est une action système envoyée par Android lorsqu'il est temps de mettre à jour le widget. Les autres actions sont à usage interne, et seront envoyés par d'autres composants de notre application.

Un widget nécessite certaines metadatas pour fonctionner, qui sont définies dans un autre fichier xml. Ce fichier est référencé dans le tag « meta-data ».

Avant de voir ce fichier, notez que l'activity de configuration du widget est définie avec un style particulier qui la fera ressembler à une boite de dialogue. Notez également le filtre à intent utilisé.

Les metadatas

Voici le contenu du fichier res/xml/napply_widget_meta.xml :

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="72dp"
    android:minHeight="72dp"
    android:initialLayout="@layout/napply_widget_layout"
    android:updatePeriodMillis="0"
    android:configure="fr.miximum.widget.WidgetConfigure" >
</appwidget-provider>

Ces metadatas apportent les informations suivantes :

  1. La taille minimale du widget à l'écran. L'accueil par défaut d'Android est une grille de 4x4 cellules, et le côté minimal par défaut d'une cellule est de 74dp (density independant pixel). La formule pour calculer la longueur du côté est : (nb cellules * 74) - 2. Android arrondira automatiquement. Ici, on voit que notre widget nécessite une cellule en hauteur, et une en largeur.
  2. Le fichier de layout utilisé. Android respecte MVC, la vue des différents composants (le layout) est décrit dans des fichiers xml. Nous référençons ici le fichier qui décrira le layout du widget.
  3. La période d'update, c'est à dire la fréquence à laquelle Android lancera l'événement APPWIDGET_UPDATE. Comme nous allons gérer les mises à jour par nous même, nous désactivons cette valeur.
  4. L'activity de configuration, dont j'ai déjà parlé.

Et notre widget, il a quelle tête ?

Jusqu'à maintenant, nous n'avons fait que déclarer l'existence de notre widget. Nous allons maintenant définir à quoi il ressemble. Plus précisément, nous allons définir les éléments qui le composent, et quels styles leur appliquer. Dans le fichier res/layout/napply_widget_layout.xml :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/napply_widget"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_margin="4dp" >

    <TextView
        android:id="@+id/nap_time"
        android:text="Erase me"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:background="@drawable/widget_bg"
        style="@style/Widget.Text"/>
</RelativeLayout>

Il s'agit d'un layout trés simple : un simple champ texte centré verticalement dans un parent. Notez que nous affectons au texte un style (Widget.Text) et un fond (widget_bg).

Voici les styles utilisés (res/values/styles.xml) :

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Widget">
        <item name="android:textColor">#fff</item>
    </style>
    <style name="Widget.Text">
        <item name="android:textSize">20dp</item>
        <item name="android:padding">7dp</item>
    </style>
</resources>

Rien de trés sorcier là-dedans. En ce qui concerne le fond, il est possible d'utiliser des images png, éventuellement améliorées avec la technique des ninepatch. Voyez cet exemple de 9patch, créé pour une autre de mes appli, et qui reprends le style du widget d'accueil d'Android.

Pour aller au plus simple, j'utiliserai ici une forme géométrique, définie dans le fichier res/drawable/widget_bg.xml :

<?xml version="1.0" encoding="UTF-8"?>
<!-- Un bête rectangle aux bords arrondis, avec un dégradé en fond et un bord blanc -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <corners android:radius="7dp" />
    <stroke android:width="2dp" android:color="#ffffff" />
    <gradient android:angle="270" android:startColor="#cc333333"
        android:endColor="#cc000000" />
</shape>

Rendez-nous le Java, on a Champomy !

J'espère que vous aimez le xml, parce que pour le moment, nous n'avons mangé que ça. Mais rassurez-vous, le Java arrive.

Dans un premier temps, nous allons définir un widget qui ne fait que s'afficher à l'écran. C'est tout ? C'est tout !

/*
 * © Copyright 2011 Thibault Jouannic . All Rights Reserved. Bla bla bla
 */

package fr.miximum.widget;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;

public class NapplyWidget extends AppWidgetProvider {

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;

        // Perform this loop procedure for each App Widget that belongs to this
        // provider
        for (int i = 0; i < N; i++) {
            int appWidgetId = appWidgetIds[i];
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    /**
     * Update the widget
     *
     * @param context
     * @param appWidgetManager
     * @param appWidgetId
     */
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {

        // Prepare widget views
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.napply_widget_layout);
        views.setTextViewText(R.id.nap_time, "Erase me");
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
}

Que fait cette classe ? D'abord, elle étend AppWidgetProvider, qui fournit des helpers à la création de widgets. La fonction « onUpdate » est appelée quand le widget reçoit un intent avec l'action APPWIDGET_UPDATE. À ce moment, la fonction d'update est appelée, qui récupère un objet layout à partir du xml via la classe RemoteViews, et qui l'affecte au widget.

Une fois cela fait, une instance de la classe « AppWidgetManager » est chargée de redessiner le widget.

Intrigué par la séquence « R.layout.napply_widget_layout » ? La classe « R » est automatiquement créée lors de la construction du projet, et contient des références à divers éléments déclarés dans le Xml.

Alors, je peux afficher mon widget ? Pas encore ! Car nous avons déclaré une activity de configuration du widget, et il faut la définir. Voici la classe WidgetConfigure :

package fr.miximum.widget;

import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class WidgetConfigure extends Activity {

    private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;

    /** Called when the activity is created */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // If the user closes window, don't create the widget
        setResult(RESULT_CANCELED);

        // Find widget id from launching intent
        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }

        // If they gave us an intent without the widget id, just bail.
        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            Log.e("Napply", "Configuration Activity: no appwidget id provided");
            finish();
        }

        configureWidget(getApplicationContext());

        // Make sure we pass back the original appWidgetId before closing the activity
        Intent resultValue = new Intent();
        resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
        setResult(RESULT_OK, resultValue);
        finish();
    }

    /**
     * Configures the created widget
     * @param context
     */
    public void configureWidget(Context context) {
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        NapplyWidget.updateAppWidget(context, appWidgetManager, mAppWidgetId);
    }
}

Cette activity est la plus simple possible, c'est à dire qu'elle ne fait… rien. Au démarrage, elle configure le widget, et se ferme automatiquement.

Ensuite, selon les besoins, il sera possible de déclarer un layout, et d'effectuer toutes les actions possibles nécessaires à l'initialisation du widget.

Et au final…

https://www.miximum.fr/wp-content/uploads/2011/03/widget1.png

Tout est maintenant prêt, il ne reste plus qu'à tester. Pour ça, il va falloir utiliser le SDK pour déclarer et lancer un émulateur Android. Ensuite, il suffira de construire le projet pour qu'il soit installé sur l'émulateur.

Enfin, sur l'émulateur, activez le widget pour obtenir ce résultat à la beauté indescriptible.

I want some action!

Notre widget est pour le moment d'une immobilité qui n'est pas sans rappeler certains parti politiques français (plutôt de gauche) ((Oulà le troll !)), aussi allons nous lui rajouter un peu d'action : en cliquant dessus, je veux afficher un message de notification.

Dans la classe « NapplyWidget », voici le nouveau corps de la méthode « updateAppWidget » :

static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {

    // Prepare widget views
    RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.napply_widget_layout);
    views.setTextViewText(R.id.nap_time, "Erase me");

    // Prepare intent to launch on widget click
    Intent intent = new Intent(context, NapplyWidget.class);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    intent.setAction(ACTION_SHOW_NOTIFICATION);

    // Launch intent on widget click
    PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
    views.setOnClickPendingIntent(R.id.napply_widget, pendingIntent);

    appWidgetManager.updateAppWidget(appWidgetId, views);
}

Nous déclarons un intent, avec une action spécifique (ACTION_SHOW_NOTIFICATION), qui sera envoyé à notre widget même. Ensuite, via la classe PendingIntent, nous indiquons que notre intent initial sera envoyé au clic sur le widget.

N'oubliez pas de définir la variable de classe :

public static final String ACTION_SHOW_NOTIFICATION = "fr.miximum.widget.SHOW_NOTIFICATION";

Puisque notre widget doit pouvoir recevoir cet intent, il ne faut pas oublier de le définir dans le Manifest :

<!-- Napply widget -->
<receiver android:name="NapplyWidget">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        <action android:name="fr.miximum.widget.SHOW_NOTIFICATION" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/napply_widget_meta" />
</receiver>

Désormais, en cliquant sur notre widget, on lui envoie une intent avec l'action SHOW_NOTIFICATION. Seulement, ce message, nous n'en faisons pour le moment rien du tout. Remédions-y :

/**
 * Handle new messages
 */
@Override
public void onReceive(Context context, Intent intent) {
    super.onReceive(context, intent);

    if (ACTION_SHOW_NOTIFICATION.equals(intent.getAction())) {
        showNotification(context);
    }
}

/**
 * Displays a notification message
 * @param context
 */
protected void showNotification(Context context) {
    CharSequence message = "Clique moi ! Clique moi ! Clique moi !";
    int duration = Toast.LENGTH_SHORT;
    Toast toast = Toast.makeText(context, message, duration);
    toast.show();
}

Le code est assez limpide, aussi me passerai-je de le commenter. ((il est midi, j'ai faim)) Encore une fois, le résultat est difficilement soutenable de beauté.

https://www.miximum.fr/wp-content/uploads/2011/03/widget_toast.png

C'est tout pour aujourd'hui. La prochaine fois, nous jouerons avec les alarmes d'Android.