Les activités

Le projet d’exemple pour ce chapitre

Vous pouvez télécharger le projet servant d’exemple pour ce chapitre : android-demo-activite.zip

Une activité (activity) représente un écran dans une application Android. Nous avons vu au chapitre précédent que l’emploi des layouts permet de garantir un rendu adapté selon le type d’appareil et les dimensions de l’écran.

Pour développer des activités, il est aussi nécessaire de comprendre que l’environnement Android se distingue du développement d’applications de bureau ou d’applications Web. En effet, l’environnement Android est généralement plus limité en ressources qu’un ordinateur classique. Il est donc important de limiter au mieux l’usage de ces ressources (batterie, mémoire…). Pour cela, le système Android gère un cycle de vie des activités en privilégiant l’économie de ressources. Même si l’utilisateur peut avoir l’impression que l’écran qui lui est présenté existe aussi longtemps que l’application s’exécute, dans la réalité, l’activité associée peut être détruite et recréée plusieurs fois durant la vie de l’application. Par exemple, si l’utilisateur répond au téléphone alors qu’une activité s’exécute, cette dernière est suspendue par le système et, si nécessaire, le système peut décider de la supprimer pour économiser des ressources. Lorsque l’utilisateur voudra reprendre l’utilisation de l’application, l’activité sera intégralement recréée.

Le développement d’activités suppose de s’astreindre à tenir compte de ce cycle de vie assez particulier afin que les applications se comportent comme attendues quelle que soit la situation.

Le cycle de vie

Note

Reportez-vous à la documentation officielle :

Les activités sont des composants, c’est-à-dire qu’il n’est pas possible d’instancier directement une activité dans le code d’une application (avec le mot-clè new par exemple). Une activité est créée et gérée par le système. Un développeur doit donc être soucieux de respecter la façon dont le système va gérer les activités de l’application. Le système Android définit un cycle de vie d’une activité : cette dernière passe par des états différents. Une activité peut être prévenue du passage dans un des états suivants :

  • initialisée (initialized)

  • créée (created)

  • démarrée (started)

  • relancée (resumed)

  • mise en pause (paused)

  • arrêtée (stopped)

  • détruite (destroyed)

Chaque passage vers un nouvel état (hormis initialisée) est signalé par un appel à une méthode de callback : onCreate, onStart, onResume, onPause, onStop, onDestroy plus la méthode onRestart pour indiquer qu’une activité qui a été arrêtée est à nouveau démarrée. Pour intervenir à un moment du cycle de vie de l’activité, il suffit de redéfinir la méthode voulue. Attention, il faut penser à appeler la super implémentation de la méthode.

Pour les méthodes onCreate, onStart, onResume, il vaut mieux appeler la super implémentation avant son propre code :

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // mon code ici
}

Pour les méthodes onPause, onStop, onDestroy, il vaut mieux appeler la super implémentation après son propre code :

@Override
protected void onDestroy() {
    // mon code ici

    super.onDestroy();
}

Le schéma ci-dessous détaille les transitions possibles entre chaque état avec l’appel des méthodes de callback.

../_images/activity_lifecycle.png

Le cycle de vie d’une activité (extrait de la documentation officielle)

Pour mieux comprendre ce schéma, prenons un exemple. Un utilisateur lance une application de consultation de mail qui doit afficher une activité pour présenter la liste des mails. Cette activité est créée par le système et ses méthodes onCreate, onStart et onResume sont appelées à la suite. Après cela, l’activité est affichée à l’écran. L’utilisateur clique sur un mail pour en consulter le détail. Cette action crée et affiche une nouvelle activité. L’activité de liste des mails est arrêtée (onPause puis onStop sont appelées) car elle n’est plus au premier plan. Puis l’utilisateur appuie sur le bouton Back qui a pour objectif de le ramener sur la liste des mails. L’activité doit être à nouveau affichée et ses méthodes onRestart, onStart, onResume sont appelées avant cela. Puis, l’utilisateur reçoit un coup de téléphone et répond. Cela amène l’application de téléphonie en premier plan. Du coup, l’activité de liste des mails est à nouveau arrêtée (onPause, onStop). Comme la communication téléphonique dure un certain temps, le système décide de libérer des ressources. Lorsque l’utilisateur raccroche et revient à l’application de mails, l’activité est entièrement recréée (onCreate, onStart, onResume). Enfin, l’utilisateur consulte la liste des applications sur son téléphone. Cela amène l’application en arrière-plan et suspend l’activité (onPause et onStop). L’utilisateur décide d’arrêter l’application de mail. L’activité est alors complètement détruite (onDestroy).

On voit avec l’exemple précédent que le cycle de vie d’une activité peut être très complexe à décrire. Cette complexité permet au développeur de gérer au mieux les ressources lorsqu’il implémente une activité. Pour chaque méthode de callback nous allons fournir quelques bonnes pratiques.

onCreate

La redéfinition de cette méthode permet de réaliser les tâches d’initialisation les plus coûteuse (comme par exemple, créer le layout d’affichage de l’activité). C’est dans cette méthode que doivent être réalisés les traitements à exécuter une seule fois pour toute la vie de l’activité.

onStart

La redéfinition de cette méthode permet de savoir quand une activité va être rendue visible à l’utilisateur. Vous pouvez, par exemple, faire des mises à jour de l’interface graphique avant affichage. Si votre activité joue une animation, cette méthode est une bonne candidate pour lancer automatiquement l’animation, soit parce que l’utilisateur démarre l’activité pour la première fois soit parce que l’activité a été arrêtée et que l’animation doit être continuée.

onResume

La redéfinition de cette méthode permet de savoir quand une activité va être accessible (y compris quand elle sort d’une pause). C’est le bon moment pour votre activité pour déclencher des opérations consommatrices de ressources (activation de la caméra, ouverture de connexions réseau…)

onPause

La redéfinition de cette méthode permet de savoir quand une activité n’est plus active pour l’utilisateur. C’est le moment de suspendre les opérations consommatrices de ressources (désactivation de la caméra, fermeture des connexions réseau…). Attention, une activité en pause peut toujours être visible de l’utilisateur. Vous ne devriez donc pas utiliser cette méthode pour modifier l’interface graphique ou stopper des animations.

onStop

La redéfinition de cette méthode permet de savoir que l’activité va être stoppée et ne sera plus visible de l’utilisateur. Si votre activité a besoin de sauvegarder des informations (notamment sur disque) pour pouvoir redémarrer convenablement, vous pouvez effectuer ces traitements à ce moment-là. Vous pouvez utiliser cette redéfinition pour stopper les opérations graphiques (animations, lecteur de vidéo…).

onDestroy

La redéfinition de cette méthode permet de savoir que l’activité va être détruite. Il s’agit de l’ultime étape et vous pouvez effectuer ici des traitements avant la suppression totale de l’activité. Attention, une activité peut être détruite par le système pour libérer temporairement des ressources. Cela ne signifie donc pas que l’activité a été simplement fermée par l’utilisateur.

Il existe une méthode de callback particulière :

onRestart

La redéfinition de cette méthode permet de savoir qu’une activité va être à nouveau démarrée après avoir été stoppée. L’appel à cette méthode se situe entre onStop et onStart. Cela permet plus facilement de faire la différence entre le démarrage normal de l’activité et son redémarrage.

Terminer une activité

Vous pouvez terminer une activité en appelant sa méthode finish. L’activité est alors détruite en passant par tous les états nécessaires. Si vous voulez savoir si l’activité est détruite suite à un appel à finish, vous pouvez tester le retour de la méthode isFinishing. Cette dernière méthode retourne false si l’activité est détruite à la demande du système pour libérer des ressources.

Gestion de l’état d’une activité

Note

Reportez-vous à la documentation officielle :

Une des grandes difficultés lorsqu’on débute le développement d’activités est la gestion de leur état. En effet, comme nous l’avons vu à la section précédente, un objet activité peut être détruit et recréé plusieurs fois alors que l’utilisateur à l’impression à chaque fois de voir le même écran. Si une activité possède un état interne alors cet état doit être sauvé et restauré pour garantir une expérience utilisateur cohérente. Pour illustrer cette situation, prenons un exemple simple. Voici un exemple d’activité avec son layout :

Une activité avec un état
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package dev.gayerie.premiereapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private TextView message;
    private int nbClics;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        message = findViewById(R.id.textMessage);
        updateMessage();
    }

    public void onBoutonClic(View v) {
        nbClics++;
        updateMessage();
    }

    private void updateMessage() {
        message.setText(String.format("Vous avez cliqué %d fois", nbClics));
    }

}
Le layout de l’activité
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textMessage"
        android:padding="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Clic"
        android:onClick="onBoutonClic"
        android:layout_gravity="center" />

</androidx.appcompat.widget.LinearLayoutCompat>

Cette application affiche un bouton et un message. À chaque fois que l’utilisateur clique sur le bouton, l’attribut nbClics est incrémenté et le message est mis à jour.

../_images/application_clic_avec_etat.png

L’application au démarrage

../_images/application_clic_avec_etat_2.png

L’application après quelques clics

Essayez cet exemple. Si vous cliquez plusieurs fois sur le bouton, le libellé est mis à jour. Mais si vous changez l’orientation de votre téléphone, vous aurez la surprise de voir à nouveau le message de départ : « Vous avez cliqué 0 fois ». Il se produira la même chose si vous changez d’application et que vous revenez ensuite sur celle-ci. Que se passe-t-il ? Le système gère votre activité comme décrit plus haut. Lorsque vous changez l’orientation de votre téléphone ou que vous revenez à l’application après l’avoir mise en tâche de fond, l’activité est entièrement recréée : il s’agit d’un nouvel objet Java. Donc l’attribut nbClics vaut à nouveau 0. Cet attribut constitue l’état de votre activité. Et à chaque fois que le système décide de supprimer votre activité, cet état est perdu. Ce comportement est rarement celui attendu par l’utilisateur. En tant que développeur, vous devez donc gérer la sauvegarde et la restauration de l’état de votre activité.

Pour cela, vous pouvez redéfinir les méthodes onSaveInstanceState et onRestoreInstanceState. La méthode onSaveInstanceState est appelée lorsque le système va détruire l’activité pour récupérer des ressources afin que celle-ci ait l’opportunité de sauver son contexte. Elle prend en paramètre un objet de type Bundle. Un bundle permet de sauvegarder des types simples (valeur booléenne, entier, chaîne de caractères…). Nous pouvons surcharger cette méthode pour placer la valeur de l’attribut nbClics dans le bundle.

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    savedInstanceState.putInt("nbClics", nbClics);
    super.onSaveInstanceState(savedInstanceState);
}

Lorsque vous redéfinissez la méthode onSaveInstanceState, vous devez impérativement appeler sa super implémentation car l’activité a besoin de sauvegarder d’autres informations d’état.

Pour récupérer les informations à la recréation, nous pouvons redéfinir la méthode onRestoreInstanceState pour, cette fois, lire les informations depuis le bundle reçu en paramètre et mettre à jour le message :

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    nbClics = savedInstanceState.getInt("nbClics");
    updateMessage();
}

Lorsque vous redéfinissez la méthode onRestoreInstanceState, vous devez impérativement appeler sa super implémentation car l’activité a besoin de restaurer d’autres informations d’état.

Après l’ajout de ces deux méthodes, vous pouvez relancer l’application et constater qu’elle fonctionne comme attendu même lorsqu’on modifie l’orientation du téléphone ou que l’application est passée temporairement en arrière-plan.

Plutôt que de redéfinir la méthode onRestoreInstanceState, nous pouvons utiliser directement la méthode onCreate qui prend en paramètre le bundle. Ce paramètre vaut null lorque l’activité est créée la première fois.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    message = findViewById(R.id.textMessage);
    if (savedInstanceState != null) {
        nbClics = savedInstanceState.getInt("nbClics");
    }
    updateMessage();
}

Astuce

La gestion de l’état est parfois une partie complexe à gérer pour le développeur. Pour nous aider, l’implémentation par défaut sauvegarde automatiquement les informations d’état de chaque vue. Si votre activité affiche un formulaire avec des champs de saisie, la valeur de ces champs est automatiquement sauvegardée et restaurée si ces champs possèdent un id donné par l’attribut android:id.

Il existe d’autres manières de conserver l’état d’une activité. L’utilisation d’un bundle est adaptée pour conserver des états simples et uniquement pour gérer les cas de recréation de votre activité pour des raisons d’économie de ressources. Si vous souhaitez disposer d’un état persistant lorsque l’utilisateur relance l’application, vous devez opter pour une solution basée sur des fichiers ou même une base de données. Le modèle par composant défini par Android JetPack propose également une approche Model/View pour gérer la sauvegarde d’état lorsqu’une application est stoppée par le système.

Notion d’intent

Note

Reportez-vous à la documentation officielle :

Un intent est un message qui est envoyé pour activer un composant Android (comme une activité). Un intent peut être émis par un composant de l’application vers un autre composant de l’application. Par exemple, une activité peut ainsi démarrer une nouvelle activité (comme nous le verrons au chapitre suivant). Mais un intent peut également servir à déclencher un composant d’une autre application. Le système Android gère ainsi une système de messagerie inter-applicative. Certains composants émettent des intents et d’autres demandent au système à être sollicités lors de l’envoi de ces intents. Cela permet un système à couplage faible et permet à une application de s’insérer facilement dans l’écosystème des applications.

Un intent peut contenir les informations suivantes :

Nom du composant

Un intent peut désigner une application spécifiquement avec son nom de package ou même un composant précis de l’application (comme une activité). Dans ce cas, on dit que l’intent est explicite. Mais il est également possible d’utiliser un intent implicite, c’est-à-dire qu’il ne fournit pas de nom de composant. Dans ce cas, le système est responsable de trouver l’application la plus appropriée pour répondre. Par exemple, on peut émettre un intent pour envoyer un mail. Le système doit ouvrir l’application la plus adaptée pour cela. Si plusieurs applications sont éligibles, le système demande à l’utilisateur celle qu’il préfère utiliser.

Action

L’action est un identifiant sous la forme d’une chaîne de caractères. Il existe des identifiants d’action prédéfinis par le système mais vous pouvez très bien créer les vôtres si nécessaire.

Data

La data se présente sous la forme d’une URI ou d’un type MIME pour désigner l’objet de l’intent.

Catégorie

La catégorie est un identifiant sous la forme d’une chaîne de caractères. Elle permet de préciser l’action.

Extras

Les extras sont en fait des paramètres sous la forme clé/valeur. Par exemple pour un intent destiné à envoyer un mail, on peut ajouter une extrait précisant le sujet du mail.

Flags

Les flags sont des méta-informations que l’on peut ajouter à l’intent pour des utilisations avancées.

Associer une activité à un intent

Lorsque l’on déclare une activité dans le fichier AndroidManifest.xml, il est possible de lui associer un filtre d’intent (intent-filter) pour indiquer au système dans quels cas cette activité doit être déclenchée. Si vous consultez le fichier AndroidManifest.xml pour un projet créé par Android Studio, vous verrez un fichier ressemblant à celui-ci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="dev.gayerie.premiereapplication">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

À partir de la ligne 13, ce ficher manifeste déclare une activité en l’associant au filtre d’intent avec l’action android.intent.action.MAIN de la catégorie android.intent.category.LAUNCHER. Il s’agit de l’intent qui est envoyé à l’application lorsque l’utilisateur lance l’application. Donc ce filtre permet au système de savoir quelle activité créer au lancement de l’application.

Une (et une seule) activité devrait être associée à ce type d’intent pour permettre à l’application de pouvoir être exécutée directement par l’utilisateur en cliquant sur son icône. Mais il est également possible d’envisager que la même activité (ou une autre) puisse être lancée à partir d’une autre action.

Un exemple assez courant est la possibilité de lancer une activité lorsque le système reçoit la directive d’afficher le contenu d’une URI. Prenons le cas du QR Code suivant :

../_images/qrcode.png

Un QR Code pour l’URI demo://test

Le QR Code ci-dessus est la représentation graphique de l’URI demo://test. Si vous utilisez une application de lecture de QR Code sur un système Android, cette dernière va décoder le QR Code et va demander au système d’afficher l’URI décodée demo://test. Pour cela, elle va émettre un intent. Le système va alors chercher une application capable de prendre en charge le schéma demo. Les schémas les plus courants comme http, https ou tel sont déjà pris en charge par des applications installées par défaut. Mais nous pouvons fournir une activité prenant spécifiquement en charge une URI de la forme demo:// grâce à l’utilisation d’un filtre d’intent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="dev.gayerie.premiereapplication">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".DemoActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="demo"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

L’activité déclarée dans le manifeste à partir de la ligne 19 est associée à un filtre d’intent pour l’action android.intent.action.VIEW et dont les données correspondent au schéma demo (ligne 24). Dit autrement, si on demande au système d’afficher une URI commençant par demo://, il démarrera notre application pour lancer l’activité DemoActivity.

L’activité DemoActivity
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package dev.gayerie.premiereapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.net.Uri;
import android.os.Bundle;
import android.widget.TextView;

public class DemoActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TextView textView = new TextView(this);
        Uri intentData = getIntent().getData();
        String message = String.format("Vous avez ouvert l'activité à partir de l'adresse %s", intentData.toString());
        textView.setText(message);

        setContentView(textView);
    }
}

Notez à la ligne 16 que l’activité peut accéder à l’intent qui a déclenché sa création grâce à la méthode getIntent. On peut ainsi facilement récupérer l’URI grâce à la propriété data. Dans notre exemple, on se contente de l’afficher à l’écran.

Note

De la même manière, vous pouvez associer une activité à l’hôte ou même au chemin d’une ressource dans l’URI. Cela permet, par exemple, de déclencher le lancement de votre application quand l’utilisateur clique sur un lien dans une page Web :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<activity android:name=".OtherDemoActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="https"
              android:host="api.mondomaine.com"
              android:path="/launch"/>
    </intent-filter>
</activity>

L’activité déclarée ci-dessus est ouverte quand le système reçoit un message lui demandant d’ouvrir l’URI https://api.mondomaine.com/launch. Vous pouvez vous contenter de placer un lien sur une page Web de votre site.

Émettre un intent implicite

Nous pouvons très facilement demander le lancement d’une activité en utilisant un intent. Android distingue les intents implicites et les intents explicites. Un intent implicite doit être résolu par le système, c’est-à-dire qu’il doit sélectionner l’application la plus appropriée pour réaliser l’action (si plusieurs applications sont éligibles, alors le système affiche une boite de dialogue à l’utilisateur pour qu’il choisisse l’application qu’il préfère lancer). A contrario, un intent explicite permet de désigner explicitement l’application, voire l’activité, que l’on souhaite exécuter. Un intent implicite permet très facilement à une application d’interagir avec les autres applications. En effet, le programme se contente de déclarer le type d’action qu’il souhaite voir réaliser et le système doit choisir l’application appropriée. Un intent explicite est généralement utilisé pour ouvrir des activités au sein de la même application et gérer les enchaînements entre les activités (comme nous le verrons au chapitre suivant).

L’émission d’un intent implicite se fait en trois étapes :

  1. On crée l’intent pour déclarer l’action et les données

  2. On vérifie qu’il existe au moins une application pour prendre en charge ce type d’intent

  3. On demande au système de lancer l’activité capable de prendre en compte notre intent.

Ci-dessous un exemple d’émission d’un intent implicite pour déclencher un appel téléphonique :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Uri numero = Uri.parse("tel:0601020304");
Intent intentAppel = new Intent(Intent.ACTION_DIAL, numero);

List<ResolveInfo> infos = getPackageManager().queryIntentActivities(intentAppel,
                                                                    PackageManager.MATCH_DEFAULT_ONLY);
if (! infos.isEmpty()) {
    startActivity(intentAppel);
} else {
    Toast.makeText(this, "Impossible d'appeler",  Toast.LENGTH_LONG).show();
}

Aux lignes 1 et 2, on crée une URI représentant le numéro de téléphone à appeler avec le schéma tel: et on crée l’intent pour une action DIAL. La ligne 4 permet d’effectuer une requête pour savoir s’il existe une ou plusieurs activités capables de prendre en charge l’intent. Pour notre code, il suffit de nous assurer que cette liste n’est pas vide pour être sûr que l’intent sera pris en charge. Puis, ligne 7, on émet l’intent grâce à la méthode startActivity et le système se charge des opérations nécessaires.