Le stockage de données

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

La plupart de nos applications auront besoin de stocker des données à un moment ou à un autre. La couleur préférée de l'utilisateur, sa configuration réseau ou encore des fichiers téléchargés sur internet. En fonction de ce que vous souhaitez faire et de ce que vous souhaitez enregistrer, Android vous fournit plusieurs méthodes pour sauvegarder des informations. Il existe deux solutions qui permettent d'enregistrer des données de manière rapide et flexible, si on exclut les bases de données :

  • Nous aborderons tout d'abord les préférences partagées, qui permettent d'associer à un identifiant une valeur. Le couple ainsi créé permet de retenir les différentes options que l'utilisateur souhaiterait conserver ou l'état de l'interface graphique. Ces valeurs pourront être partagées entre plusieurs composants. Encore mieux, Android propose un ensemble d'outils qui permettront de faciliter grandement le travail et d'unifier les interfaces graphiques des activités dédiées à la sauvegarde des préférences des utilisateurs.
  • Il peut aussi arriver qu'on ait besoin d'écrire ou de lire des fichiers qui sont stockés sur le terminal ou sur un périphérique externe.

Ici, on ne parlera pas des bases de données. Mais bientôt, promis.

Préférences partagées

Utile voire indispensable pour un grand nombre d'applications, pouvoir enregistrer les paramètres des utilisateurs leur permettra de paramétrer de manière minutieuse vos applications de manière à ce qu'ils obtiennent le rendu qui convienne le mieux à leurs exigences.

Les données partagées

Le point de départ de la manipulation des préférences partagées est la classe SharedPreferences. Elle possède des méthodes permettant d'enregistrer et récupérer des paires de type identifiant-valeur pour les types de données primitifs, comme les entiers ou les chaînes de caractères. L'avantage réel étant bien sûr que ces données sont conservées même si l'application est arrêtée ou tuée. Ces préférences sont de plus accessibles depuis plusieurs composants au sein d'une même application.

Il existe trois façons d'avoir accès aux SharedPreferences :

  • La plus simple est d'utiliser la méthode statique SharedPreferences PreferenceManager.getDefaultSharedPreferences(Context context).
  • Si vous désirez utiliser un fichier standard par activité, alors vous pourrez utiliser la méthode SharedPreferences getPreferences(int mode).
  • En revanche, si vous avez besoin de plusieurs fichiers que vous identifierez par leur nom, alors utilisez SharedPreferences getSharedPreferences (String name, int mode)name sera le nom du fichier.

En ce qui concerne le second paramètre, mode, il peut prendre trois valeurs :

  • Context.MODE_PRIVATE, pour que le fichier créé ne soit accessible que par l'application qui l'a créé.
  • Context.MODE_WORLD_READABLE, pour que le fichier créé puisse être lu par n'importe quelle application.
  • Context.MODE_WORLD_WRITEABLE, pour que le fichier créé puisse être lu et modifié par n'importe quelle application.

Petit détail, appeler SharedPreferences PreferenceManager.getDefaultSharedPreferences(Context context) revient à appeler SharedPreferences getPreferences(MODE_PRIVATE) et utiliser SharedPreferences getPreferences(int mode) revient à utiliser SharedPreferences getSharedPreferences (NOM_PAR_DEFAUT, mode) avec NOM_PAR_DEFAUT un nom généré en fonction du package de l'application.

Afin d'ajouter ou de modifier des couples dans un SharedPreferences, il faut utiliser un objet de type SharedPreference.Editor. Il est possible de récupérer cet objet en utilisant la méthode SharedPreferences.Editor edit() sur un SharedPreferences.

Si vous souhaitez ajouter des informations, utilisez une méthode du genre SharedPreferences.Editor putX(String key, X value) avec X le type de l'objet, key l'identifiant et value la valeur associée. Il vous faut ensuite impérativement valider vos changements avec la méthode boolean commit().

Les préférences partagées ne fonctionnent qu'avec les objets de type boolean, float, int, long et String.

Par exemple, pour conserver la couleur préférée de l'utilisateur, il n'est pas possible d'utiliser la classe Color puisque seuls les types de base sont acceptés, alors on pourrait conserver la valeur de la couleur sous la forme d'une chaîne de caractères :

1
2
3
4
5
6
7
8
public final static String FAVORITE_COLOR = "fav color";



SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(FAVORITE_COLOR, "FFABB4");
editor.commit();

De manière naturelle, pour récupérer une valeur, on peut utiliser la méthode X getX(String key, X defValue) avec X le type de l'objet désiré, key l'identifiant de votre valeur et defValue une valeur que vous souhaitez voir retournée au cas où il n'y ait pas de valeur associée à key :

1
2
3
// On veut la chaîne de caractères d'identifiant FAVORITE_COLOR
// Si on ne trouve pas cette valeur, on veut rendre "FFFFFF"
String couleur = preferences.getString(FAVORITE_COLOR, "FFFFFF");

Si vous souhaitez supprimer une préférence, vous pouvez le faire avec SharedPreferences.Editor removeString(String key), ou, pour radicalement supprimer toutes les préférences, il existe aussi SharedPreferences.Editor clear().

Enfin, si vous voulez récupérer toutes les données contenues dans les préférences, vous pouvez utiliser la méthode Map<String, ?> getAll().

Des préférences prêtes à l'emploi

Pour enregistrer vos préférences, vous pouvez très bien proposer une activité qui permet d'insérer différents paramètres (voir figure suivante). Si vous voulez développer vous-mêmes l'activité, grand bien vous fasse, ça fera des révisions, mais sachez qu'il existe un framework pour vous aider. Vous en avez sûrement déjà vus dans d'autres applications. C'est d'ailleurs un énorme avantage d'avoir toujours un écran similaire entre les applications pour la sélection des préférences.

L'activité permettant de choisir des paramètres pour le Play Store

Ce type d'activités s'appelle les « PreferenceActivity ». Un plus indéniable ici est que chaque couple identifiant/valeur est créé automatiquement et sera récupéré automatiquement, d'où un gain de temps énorme dans la programmation. La création se fait en plusieurs étapes, nous allons voir la première, qui consiste à établir une interface graphique en XML.

Étape 1 : en XML

La racine de ce fichier doit être un PreferenceScreen.

Comme ce n'est pas vraiment un layout, on le définit souvent dans /xml/preference.xml.

Tout d'abord, il est possible de désigner des catégories de préférences. Une pour les préférences destinées à internet par exemple, une autre pour les préférences esthétiques, etc. On peut ajouter des préférences avec le nœud PreferenceCategory. Ce nœud est un layout, il peut donc contenir d'autre vues. Il ne peut prendre qu'un seul attribut, android:title, pour préciser le texte qu'il affichera.

Ainsi le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
  <PreferenceCategory android:title="Réseau">

  </PreferenceCategory>
  <PreferenceCategory android:title="Luminosité">

  </PreferenceCategory>
  <PreferenceCategory android:title="Couleurs">

  </PreferenceCategory>
</PreferenceScreen>

… donne le résultat visible à la figure suivante.

Le code en image

Nous avons nos catégories, il nous faut maintenant insérer des préférences ! Ces trois vues ont cinq attributs en commun :

  • android:key est l'identifiant de la préférence partagée. C'est un attribut indispensable, ne l'oubliez jamais.
  • android:title est le titre principal de la préférence.
  • android:summary est un texte secondaire qui peut être plus long et qui explique mieux ce que veut dire cette préférence.
  • Utilisez android:dependency, si vous voulez lier votre préférence à une autre activité. Il faut y insérer l'identifiant android:key de la préférence dont on dépend.
  • android:defaultValue est la valeur par défaut de cette préférence.

Il existe au moins trois types de préférences, la première étant une case à cocher avec CheckBoxPreference, avec true ou false comme valeur (soit la case est cochée, soit elle ne l'est pas).

À la place du résumé standard, vous pouvez déclarer un résumé qui ne s'affiche que quand la case est cochée, android:summaryOn, ou uniquement quand la case est décochée, android:summaryOff.

1
2
3
4
5
6
<CheckBoxPreference 
  android:key="checkBoxPref"
  android:title="Titre"
  android:summaryOn="Résumé quand sélectionné" 
  android:summaryOff="Résumé quand pas sélectionné" 
  android:defaultValue="true"/>

Ce qui donne la figure suivante.

Regardez la première préférence, la case est cochée par défaut et c'est le résumé associé qui est affiché

Le deuxième type de préférences consiste à permettre à l'utilisateur d'insérer du texte avec EditTextPreference, qui ouvre une boîte de dialogue contenant un EditText permettant à l'utilisateur d'insérer du texte. On retrouve des attributs qui vous rappellerons fortement le chapitre sur les boîtes de dialogue. Par exemple, android:dialogTitle permet de définir le texte de la boîte de dialogue, alors que android:negativeButtonText et android:positiveButtonText permettent respectivement de définir le texte du bouton à droite et celui du bouton à gauche dans la boîte de dialogue.

1
2
3
4
5
6
7
8
<EditTextPreference 
  android:key="editTextPref"
  android:dialogTitle="Titre de la boîte"
  android:positiveButtonText="Je valide !"
  android:negativeButtonText="Je valide pas !"
  android:title="Titre"
  android:summary="Résumé"
  android:dependency="checkBoxPref" />

Ce qui donne la figure suivante.

Le code en image

De plus, comme vous avez pu le voir, ce paramètre est lié à la CheckBoxPreference précédente par l'attribut android:dependency="checkBoxPref", ce qui fait qu'il ne sera accessible que si la case à cocher de checkBoxPref est activée, comme à la figure suivante.

Le paramètre n'est accessible que si la case est cochée

De plus, comme nous l'avons fait avec les autres boîtes de dialogue, il est possible d'imposer un layout à l'aide de l'attribut android:dialogLayout.

Le troisième type de préférences est un choix dans une liste d'options avec ListPreference. Dans cette préférence, on différencie ce qui est affiché de ce qui est réel. Pratique pour traduire son application en plusieurs langues ! Encore une fois, il est possible d'utiliser les attributs android:dialogTitle, android:negativeButtonText et android:positiveButtonText. Les données de la liste que lira l'utilisateur sont à présenter dans l'attribut android:entries, alors que les données qui seront enregistrées sont à indiquer dans l'attribut android:entryValues. La manière la plus facile de remplir ces attributs se fait à l'aide d'une ressource de type array, par exemple pour la liste des couleurs :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<resources>
  <array name="liste_couleurs_fr">
    <item>Bleu</item>
    <item>Rouge</item>
    <item>Vert</item>
  </array>
  <array name="liste_couleurs">
    <item>blue</item>
    <item>red</item>
    <item>green</item>
  </array>
</resources>

Qu'on peut ensuite fournir aux attributs susnommés :

1
2
3
4
5
6
<ListPreference 
  android:key="listPref"
  android:dialogTitle="Choisissez une couleur"
  android:entries="@array/liste_couleurs_fr"
  android:entryValues="@array/liste_couleurs"
  android:title="Choisir couleur" />

Ce qui donne la figure suivante.

Le code en image

On a sélectionné « Vert », ce qui signifie que la valeur enregistrée sera green.

Étape 2 : dans le Manifest

Pour recevoir cette nouvelle interface graphique, nous avons besoin d'une activité. Il nous faut donc la déclarer dans le Manifest si on veut pouvoir y accéder avec les intents. Cette activité sera déclarée comme n'importe quelle activité :

1
2
3
4
<activity
  android:name=".PreferenceActivityExample"
  android:label="@string/title_activity_preference_activity_example" >
</activity>

Étape 3 : en Java

Notre activité sera en fait de type PreferenceActivity. On peut la traiter comme une activité classique, sauf qu'au lieu de lui assigner une interface graphique avec setContentView, on utilise void addPreferencesFromResource(int preferencesResId) en lui assignant notre layout :

1
2
3
4
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  addPreferencesFromResource(R.xml.preference);
}

Manipulation des fichiers

On a déjà vu comment manipuler certains fichiers précis à l'aide des ressources, mais il existe aussi des cas de figure où il faudra prendre en compte d'autres fichiers, par exemple dans le cas d'un téléchargement ou de l'exploration de fichiers sur la carte SD d'un téléphone. En théorie, vous ne serez pas très dépaysés ici puisqu'on manipule en majorité les mêmes méthodes qu'en Java. Il existe bien entendu quand même des différences.

Il y a deux manières d'utiliser les fichiers : soit sur la mémoire interne du périphérique à un endroit bien spécifique, soit sur une mémoire externe (par exemple une carte SD). Dans tous les cas, on part toujours du Context pour manipuler des fichiers.

Rappels sur l'écriture et la lecture de fichiers

Ce n'est pas un sujet forcément évident en Java puisqu'il existe beaucoup de méthodes qui permettent d'écrire et de lire des fichiers en fonction de la situation.

Le cas le plus simple est de manipuler des flux d'octets, ce qui nécessite des objets de type FileInputStream pour lire un fichier et FileOutputStream pour écrire dans un fichier. La lecture s'effectue avec la méthode int read() et on écrit dans un fichier avec void write(byte[] b). Voici un programme très simple qui lit dans un fichier puis écrit dans un autre fichier :

 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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
  public static void main(String[] args) throws IOException {

    FileInputStream in = null;
    FileOutputStream out = null;

    try {
      in = new FileInputStream("entree.txt");
      out = new FileOutputStream("sortie.txt");
      int octet;

      // La méthode read renvoie -1 dès qu'il n'y a plus rien à lire
      while ((octet = in.read()) != -1) {
        out.write(octet);
      }
      if (in != null)
        in.close();

      if (out != null)
        out.close();
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

En interne

L'avantage ici est que la présence des fichiers dépend de la présence de l'application. Par conséquent, les fichiers seront supprimés si l'utilisateur désinstalle l'activité. En revanche, comme la mémoire interne du téléphone risque d'être limitée, on évite en général de placer les plus gros fichiers de cette manière.

Afin de récupérer un FileOutputStream qui pointera vers le bon répertoire, il suffit d'utiliser la méthode FileOutputStream openFileOutput (String name, int mode) avec name le nom du fichier et mode le mode dans lequel ouvrir le fichier. Eh oui, encore une fois, il existe plusieurs méthodes pour ouvrir un fichier :

  • MODE_PRIVATE permet de créer (ou de remplacer, d'ailleurs) un fichier qui sera utilisé uniquement par l'application.
  • MODE_WORLD_READABLE pour créer un fichier que même d'autres applications pourront lire.
  • MODE_WORLD_WRITABLE pour créer un fichier où même d'autres applications pourront lire et écrire.
  • MODE_APPEND pour écrire à la fin d'un fichier préexistant, au lieu de créer un fichier.

Par exemple, pour écrire mon pseudo dans un fichier, je ferai :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FileOutputStream output = null;        
String userName = "Apollidore";

try {
  output = openFileOutput(PRENOM, MODE_PRIVATE);
  output.write(userName.getBytes());
  if(output != null)
    output.close();
} catch (FileNotFoundException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

De manière analogue, on peut retrouver un fichier dans lequel lire à l'aide de la méthode FileInputStream openFileInput (String name).

N'essayez pas d'insérer des « / » ou des « \ » pour mettre vos fichiers dans un autre répertoire, sinon les méthodes renverront une exception.

Ensuite, il existe quelques méthodes qui permettent de manipuler les fichiers au sein de cet emplacement interne, afin d'avoir un peu plus de contrôle. Déjà, pour retrouver cet emplacement, il suffit d'utiliser la méthode File getFilesDir(). Pour supprimer un fichier, on peut faire appel à boolean deleteFile(String name) et pour récupérer une liste des fichiers créés par l'application, on emploie String[] fileList().

Travailler avec le cache

Les fichiers normaux ne sont supprimés que si quelqu'un le fait, que ce soit vous ou l'utilisateur. A contrario, les fichiers sauvegardés avec le cache peuvent aussi être supprimés par le système d'exploitation afin de libérer de l'espace. C'est un avantage, pour les fichiers qu'on ne veut garder que temporairement.

Pour écrire dans le cache, il suffit d'utiliser la méthode File getCacheDir() pour récupérer le répertoire à manipuler. De manière générale, on évite d'utiliser trop d'espace dans le cache, il s'agit vraiment d'un espace temporaire de stockage pour petits fichiers.

Ne vous attendez pas forcément à ce qu'Android supprime les fichiers, il ne le fera que quand il en aura besoin, il n'y a pas de manière de prédire ce comportement.

En externe

Le problème avec le stockage externe, c'est qu'il n'existe aucune garantie que vos fichiers soient présents. L'utilisateur pourra les avoir supprimés ou avoir enlevé le périphérique de son emplacement. Cependant, cette fois la taille disponible de stockage est au rendez-vous ! Enfin, quand nous parlons de périphérique externe, nous parlons principalement d'une carte SD, d'une clé USB… ou encore d'un ordinateur !

Pour écrire sur un périphérique externe, il vous faudra ajouter la permission WRITE_EXTERNAL_STORAGE. Pour ce faire, il faut rajouter la ligne suivante à votre Manifest : <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />.

Tout d'abord, pour vérifier que vous avez bien accès à la mémoire externe, vous pouvez utiliser la méthode statique String Environment.getExternalStorageState(). La chaîne de caractères retournée peut correspondre à plusieurs constantes, dont la plus importante reste Environment.MEDIA_MOUNTED pour savoir si le périphérique est bien monté et peut être lu (pour un périphérique bien monté mais qui ne peut pas être lu, on utilisera Environment.MEDIA_MOUNTED_READ_ONLY) :

1
2
3
4
if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
  // Le périphérique est bien monté
else
  // Le périphérique n'est pas bien monté ou on ne peut écrire dessus

Vous trouverez d'autres statuts à utiliser dans la documentation.

Afin d'obtenir la racine des fichiers du périphérique externe, vous pouvez utiliser la méthode statique File Environment.getExternalStorageDirectory(). Cependant, il est conseillé d'écrire des fichiers uniquement à un emplacement précis : /Android/data/<votre_package>/files/. En effet, les fichiers qui se trouvent à cet emplacement seront automatiquement supprimés dès que l'utilisateur effacera votre application.

Partager des fichiers

Il arrive aussi que votre utilisateur veuille partager la musique qu'il vient de concevoir avec d'autres applications du téléphone, pour la mettre en sonnerie par exemple. Ce sont des fichiers qui ne sont pas spécifiques à votre application ou que l'utilisateur ne souhaitera pas supprimer à la désinstallation de l'application. On va donc faire en sorte de sauvegarder ces fichiers à des endroits spécifiques. Une petite sélection de répertoires : pour la musique on mettra les fichiers dans /Music/, pour les téléchargements divers on utilisera /Download/ et pour les sonneries de téléphone on utilisera /Ringtones/.

Application

Énoncé

Très simple, on va faire en sorte d'écrire votre pseudo dans deux fichiers : un en stockage interne, l'autre en stockage externe. N'oubliez pas de vérifier qu'il est possible d'écrire sur le support externe !

Détails techniques

Il existe une constante qui indique que le périphérique est en lecture seule (et que par conséquent il est impossible d'écrire dessus), c'est la constante Environment.MEDIA_MOUNTED_READ_ONLY.

Si un fichier n'existe pas, vous pouvez le créer avec boolean createNewFile().

Ma solution

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {
  private String PRENOM = "prenom.txt";
  private String userName = "Apollidore";
  private File mFile = null;

  private Button mWrite = null;
  private Button mRead = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // On crée un fichier qui correspond à l'emplacement extérieur
    mFile = new File(Environment.getExternalStorageDirectory().getPath() + "/Android/data/ " + getPackageName() + "/files/" + PRENOM);

    mWrite = (Button) findViewById(R.id.write);
    mWrite.setOnClickListener(new View.OnClickListener() {

      public void onClick(View pView) {
        try {
          // Flux interne
          FileOutputStream output = openFileOutput(PRENOM, MODE_PRIVATE);

          // On écrit dans le flux interne
          output.write(userName.getBytes());

          if(output != null)
            output.close();

          // Si le fichier est lisible et qu'on peut écrire dedans
          if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
              && !Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment.getExternalStorageState())) {
            // On crée un nouveau fichier. Si le fichier existe déjà, il ne sera pas créé
            mFile.createNewFile();
            output = new FileOutputStream(mFile);
            output.write(userName.getBytes());
            if(output != null)
              output.close();
          }
        } catch (FileNotFoundException e) {
          e.printStackTrace();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    });

    mRead = (Button) findViewById(R.id.read);
    mRead.setOnClickListener(new View.OnClickListener() {

      public void onClick(View pView) {
        try {
          FileInputStream input = openFileInput(PRENOM);
          int value;
          // On utilise un StringBuffer pour construire la chaîne au fur et à mesure
          StringBuffer lu = new StringBuffer();
          // On lit les caractères les uns après les autres
          while((value = input.read()) != -1) {
            // On écrit dans le fichier le caractère lu
            lu.append((char)value);
          }
          Toast.makeText(MainActivity.this, "Interne : " + lu.toString(), Toast.LENGTH_SHORT).show();
          if(input != null)
            input.close();

          if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            lu = new StringBuffer();
            input = new FileInputStream(mFile);
            while((value = input.read()) != -1)
              lu.append((char)value);

            Toast.makeText(MainActivity.this, "Externe : " + lu.toString(), Toast.LENGTH_SHORT).show();
            if(input != null)
              input.close();
          }

        } catch (FileNotFoundException e) {
          e.printStackTrace();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    });
  }
}

  • Il est possible d'enregistrer les préférences de l'utilisateur avec la classe SharedPreferences.
  • Pour permettre à l'utilisateur de sélectionner ses préférences, on peut définir une PreferenceActivity qui facilite le processus. On peut ainsi insérer des CheckBox, des EditText, etc.
  • Il est possible d'enregistrer des données dans des fichiers facilement, comme en Java. Cependant, on trouve deux endroits accessibles : en interne (sur un emplacement mémoire réservé à l'application sur le téléphone) ou en externe (sur un emplacement mémoire amovible, comme par exemple une carte SD).
  • Enregistrer des données sur le cache permet de sauvegarder des fichiers temporairement.