Quand on débute sous Android, ce n’est jamais facile de choisir quelle technique utiliser afin de partager ses données, à travers cet article, je vais tenter de vous expliquer quelques techniques qui peuvent être utilisées et leurs avantages et/ou inconvénients.
Il existe deux grandes méthodes pour partager vos données :
- Partagez vos données sans enregistrer sur le matériel
- Enregistrer les données sur le matériel
Partagez vos données sans enregistrer sur le matériel
Il est possible de partager des données entre les activités grâce à la mémoire étant donné que, dans la plupart des cas, les deux activités se déroulent dans le même processus. On retrouve principalement deux types de partage de mémoire les intents et les singletons.
Envoyer des données grâce aux intents
Les Intents permettent de communiquer entre les différentes activités de notre application, mais aussi du téléphone. Ce sont des actions désirées. Elle permet par exemple au clic sur un bouton de lancer une nouvelle activité. Lors de ce lancement on peut préciser si on le souhaite des paramètres. On peut donc ainsi partager des données entre deux activités.
Trêve de bavardages, voici un exemple permettant de lancer une activité depuis une autre et permettant de transférer une liste chaînée.
LinkedList<String> list = new LinkedList<String>(); Intent intent = new Intent(MainActivity.this, SecondActivity.class); intent.putExtra("list", list); startActivity(intent);
Ici, on utilise le constructeur suivant de la classe Intent :
Intent(Context packageContext, Class<?> cls)
On fournit donc en paramètre, le nom de l’activité courante et le nom de la classe que l’on veut appeler. Attention, afin que lors de la compilation la classe précisée soit trouvée, il faut l’ajouter dans le manifest de votre projet.
Ensuite, on ajoute la liste doublement chaînée à l’intent, grâce à la méthode putExtra(String, Bundle value). Enfin dans la seconde activité, afin de récupérer les données, il faudra faire :
LinkedList<String> list = (LinkedList<String>) getIntent().getSerializableExtra("list");
Petite aparté sur Serializable ou Parcelable
Afin de pouvoir passer des objets dans un Intent, il faut que l’objet en question implémente Serializable ou Parcelable.
Remarque : Il est tentant d’utiliser Serializable (comme il est plus facile à implémenter), toutefois cela présente un gros défaut : Serializable est lent. En général, on préfère donc implémenter l’interface Parcelable, bien que plus complexe, cela permet d’avoir des gains de vitesses considérables par rapport à l’interface Serializable.
Utiliser un singleton
Afin de pouvoir partager facilement des données entre plusieurs activités, on peut utiliser une classe pour contenir les données. On utilise alors le design pattern singleton. Regardons ce que nous dit Wikipédia :
En génie logiciel, le singleton est un patron de conception (design pattern) dont l’objectif est de restreindre l’instanciation d’une classe à un seul objet (ou bien à quelques objets seulement). Il est utilisé lorsque l’on a besoin d’exactement un objet pour coordonner des opérations dans un système.
Le principe
On implémente le singleton en écrivant une classe qui va contenir un élément static qui sera instancié une seule fois. On aura alors une méthode statique qui renverra cette instance. Dans beaucoup de langages objet, il faudra veiller à ce que le constructeur de la classe soit privé, afin de s’assurer que la classe ne puisse être instanciée autrement que par la méthode prévue.
Et voici, donc l’implémentation de la classe précédente :
public class DataHolder { private String data; private static final DataHolder holder = new DataHolder(); public static DataHolder getInstance() { return holder; } public String getData() { return data; } public void setData(String data) { this.data = data; } }
Vous pouvez bien sûr changer la variable data parce que vous voulez, LinkedList ou autre joyeusetés du genre !
Après pour accéder à la classe depuis une activité, il vous suffira de faire :
String data = DataHolder.getInstance().getData();
Enregistrer les données sur le matériel
L’idée est simple, on enregistre les données avant le lancement de l’activité nécessitant ces dernières.
- Avantages: On peut lancer l’activité de n’importe où, comme les données sont écrites sur le disque, il n’y aura aucun souci
- Problèmes: C’est assez lourd à mettre en place et est bien sûr plus lent qu’un simple accès mémoire.
Pour sauvegarder des données persistantes, il existe quelques solutions : la classe SharedPreferences, le stockage interne ou externe (carte SD) ou en utilisant une base de données, ie SQLite.
Sauvegarder des données en utilisant la classe SharedPreferences
Cette classe est généralement utilisé pour enregistrer les préférences de son application. On stocke les données grâce à des associations clé <=> valeur.
Pour obtenir une instance de cette classe, on a deux possibilités :
- Utiliser un fichier standard par activité, alors on se sert de la méthode getPreferences(int mode).
- Au contraire, si vous faut plusieurs fichiers par activité, alors on identifiera ces derniers par leur nom, et on a recours à la méthode getSharedPreferences (String name, int mode) où name sera le nom du fichier.
Les modes d’utilisation des SharedPreferences
Comme vous avez pu le voir, chaque méthode prend en paramètre un mode, voici les modes disponibles et leurs effets :
- 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.
Maintenant, que l’on sait, quelle méthode appeler, on va voir, comment réaliser la sauvegarde, pour écrire les valeurs, on réalise cela en 3 étapes :
- On appelle la fonction edit() pour obtenir un objet SharedPreferences.Editor.
- On utilise ce dernier afin d’y ajouter des valeurs, en utilisant des méthodes comme putString().
- On commit, c’est-à-dire valide les changements grâce à la fonction, attention : commit()
Utilisation des SharedPreferences
Et comme un exemple vaut mieux que 1000 mots, let’s do this :
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ... // Restauration des préférences SharedPreferences settings = getSharedPreferences("PrefFile", 0); String username = settings.getString("username", "defaultValue"); } @Override protected void onStop(){ super.onStop(); // Sauvegarde des préférences SharedPreferences settings = getSharedPreferences("PrefFile", 0); SharedPreferences.Editor editor = settings.edit(); editor.putString("username", username); editor.commit(); } }
Dans la fonction onCreate(), on restaure les valeurs saisies précédemment, si elle n’existe pas la fonction getString(), renverra la valeur par défaut, à savoir le second argument. La fonction onStop() est appelée lorsque l’activité courante n’est plus visible pour l’utilisateur. Ce cas arrive quand une nouvelle activité est lancée, une autre activité prend la main (à la réception d’un SMS par exemple) ou encore l’activité est détruite.
Sauvegarder des données dans une base de données SQLite
Contrairement à MySQL par exemple, SQLite ne nécessite pas de serveur pour fonctionner, ce qui signifie que son exécution se fait dans le même processus que celui de l’application. Par conséquent, une opération lourde lancée dans la base de données aura des conséquences visibles sur les performances de votre application. Il faudra donc faire attention à la manière d’implémenter notre bdd afin de ne pas pénaliser le restant de votre exécution.
SQLite a été inclus dans le cœur même d’Android, c’est pourquoi chaque application peut avoir sa propre base. De manière générale, les bases de données sont stockées dans les répertoires de la forme
/data/data/<package>/databases
Il est possible d’avoir plusieurs bases de données par application, cependant chaque fichier créé l’est selon le mode MODE_PRIVATE, par conséquent les bases ne sont accessibles qu’au sein de l’application elle-même. Notez que ce n’est pas pour autant qu’une base de données ne peut pas être partagée avec d’autres applications.
Exemple d’une base de données SQLite pour des articles
Nous allons réaliser, un exemple de base de données, permettant de gérer des articles. Tout d’abord, nous avons besoin de savoir de quoi est composé un article, on retrouve donc :
- Un ID : integer, ai
- Un titre : string
- Un auteur : string
- Du contenu : string
- Une date de publication : datetime
Représentation de l’objet Article
On peut donc représenter l’objet afin de faciliter l’utilisation de notre base de données, on retrouve donc un objet java classique, avec les champs vus au-dessus, un constructeur, des accesseurs et mutateurs ainsi qu’une méthode toString() :
public class Article { private int id; private String title; private String author; private String content; private String date; public Article() { } public Article(int id, String title, String author, String content, String date) { super(); this.id = id; this.title = title; this.author = author; this.content = content; this.date = date; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getDate() { return date; } public void setDate(String date) { this.date = date; } @Override public String toString() { return "Article [id=" + id + ", title=" + title + ", author=" + author + ", content=" + content + ", date=" + date + "]"; } }
Utilisation de la classe SQLiteOpenHelper
On va utiliser la classe SQLiteOpenHelper d’Android afin de gérer la création de notre base de données.
import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; public class ArticleOpenHelper extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 1; private static final String ARTICLE_TABLE_NAME = "article"; private static final String ID = "id"; private static final String TITLE = "title"; private static final String AUTHOR = "author"; private static final String CONTENT = "content"; private static final String DATE = "date"; private static final String ARTICLE_TABLE_CREATE = "CREATE TABLE " + ARTICLE_TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + TITLE + " TEXT NOT NULL, " + AUTHOR + " TEXT NOT NULL, " + CONTENT + " TEXT NOT NULL, " + DATE + " DATETIME);"; public ArticleOpenHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(ARTICLE_TABLE_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + ARTICLE_TABLE_NAME + ";"); onCreate(db); } }
On va maintenant utiliser deux classes :
- ContentValue, cette classe est utilisée pour stocker et envoyer un ensemble de valeurs que le ContentResolver va pouvoir traiter.
- Cursor, c’est une interface Input/Output, c’est-à-dire qui permet de réaliser des lectures/écritures de l’ensemble des résultats retournés par une requête vers la base de données.
Grâce à ces deux classes, on va pouvoir être capable de réaliser les actions classiques qu’on réalise avec une base de données, c’est-à-dire : sélection, insertion, suppression et modification.
Voici un exemple complet de notre base de données
import java.util.LinkedList; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; public class ArticleDataBase { private static final int DATABASE_VERSION = 1; private static final String DBNAME = "articles.db"; private static final String ARTICLE_TABLE_NAME = "article"; private static final String ID = "id"; private static final String TITLE = "title"; private static final String AUTHOR = "author"; private static final String CONTENT = "content"; private static final String DATE = "date"; private SQLiteDatabase bdd; private ArticleOpenHelper articleBaseSQLite; public ArticleDataBase(Context context){ articleBaseSQLite = new ArticleOpenHelper(context, DBNAME, null, DATABASE_VERSION); } public void open(){ bdd = articleBaseSQLite.getWritableDatabase(); } public void close(){ bdd.close(); } public SQLiteDatabase getBDD(){ return bdd; } public long insert(Article article){ ContentValues values = new ContentValues(); values.put(ID, article.getId()); values.put(TITLE, article.getTitle()); values.put(AUTHOR, article.getAuthor()); values.put(CONTENT, article.getContent()); values.put(DATE, article.getDate()); return bdd.insert(ARTICLE_TABLE_NAME, null, values); } public int update(int id, Article article){ ContentValues values = new ContentValues(); values.put(ID, article.getId()); values.put(TITLE, article.getTitle()); values.put(AUTHOR, article.getAuthor()); values.put(CONTENT, article.getContent()); values.put(DATE, article.getDate()); return bdd.update(ARTICLE_TABLE_NAME, values, ID + " = " +id, null); } public int remove(int id){ return bdd.delete(ARTICLE_TABLE_NAME, ID + " = " +id, null); } public Article selectWithID(String[] args) { Cursor cursor = bdd.rawQuery("SELECT * from " + ARTICLE_TABLE_NAME + " WHERE id = ? ", args); Article article = cursorToArticle(cursor, false); cursor.close(); return article; } public LinkedList<Article> selectAll(){ LinkedList<Article> articles = new LinkedList<Article>(); Cursor cursor = bdd.rawQuery("SELECT * from " + ARTICLE_TABLE_NAME, null); if(cursor.getCount() != 0){ for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { Article article = cursorToArticle(cursor, true); articles.add(article); } } cursor.close(); return articles; } public void displayArticles() { LinkedList<Article> articles = selectAll(); for (int i = 0; i < articles.size(); i++) { System.out.println(articles.get(i)); } } private Article cursorToArticle(Cursor c, boolean multipleResult){ if(!multipleResult) { c.moveToFirst(); } Article article = new Article(); article.setId(c.getInt(0)); article.setTitle(c.getString(1)); article.setAuthor(c.getString(2)); article.setContent(c.getString(3)); article.setDate(c.getString(4)); return article; } }
Testons l’utilisation de notre base de données
Enfin, on teste nos classes et on affiche le résultat dans la console, grâce à la commande System.out.println() de Java.
import android.app.Activity; import android.os.Bundle; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArticleDataBase db = new ArticleDataBase(this); Article a = new Article(1, "Transmettre des données entre activités", "Lud00", "Lorem ipsum", "23/05/2015"); Article a2 = new Article(2, "Apprendre le CSS3 grâce aux animates", "Lud00", "Lorem ipsum", "24/05/2015"); db.open(); db.insert(a); db.insert(a2); db.displayArticles(); db.close(); } }
Sauvegarder les données dans un fichier
On peut également sauvegarder les données dans un fichier de manière interne ou externe (c’est-à-dire sur une carte SD).
Utiliser le stockage interne de son smartphone
On peut sauvegarder les fichiers directement dans la mémoire du téléphone par défaut, ces fichiers sont privés à notre application et aucune autre application ne peut y accéder, tout comme l’utilisateur.
Ainsi quand l’utilisateur désinstalle notre application, ces fichiers sont supprimés. Afin de réaliser une telle sauvegarde c’est plutôt simple, on appelle la fonction openFileOutput(), elle prend en paramètre le nom du fichier ainsi que le mode de sauvegarde. La fonction retourne alors un objet FileOutputStream, grâce à ce dernier, on peut facilement écrire des données dans le fichier, une fois les données sauvegardées, il faut bien sur refermer le flux.
On peut donc facilement réaliser une fonction afin de sauvegarder une chaîne dans un fichier :
public void saveData() { FileOutputStream fos; try { fos = openFileOutput(FILENAME, Context.MODE_PRIVATE); for (String str : data) { fos.write(str.getBytes()); } fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
Remarque : Par défaut c’est donc le MODE_PRIVATE dans ce mode le fichier est créé ou remplacé par un du même nom à chaque sauvegarde. Il existe bien sûr d’autres modes : MODE_APPEND, MODE_WORLD_READABLE, et MODE_WORLD_WRITEABLE. Le mode append comme son nom l’indique permet d’ajouter des données à la fin du fichier sans en recréer un nouveau. Les autres modes sont déconseillé d’utilisation par android depuis l’api 17, en effet ils permettent à d’autres applications de lire ou d’écrire des données dans nos fichiers. Faites donc très attention si vous voulez les utiliser !
Lire le contenu d’un fichier
Pour réaliser la lecture du fichier et donc pouvoir charger des données, ça n’est pas très différent, en effet on va ouvrir le fichier grâce à la méthode openFileInput(), cette dernière retourne un objet FileInputStream, on l’utilise alors pour lire les données et on ferme l’objet quand on a fini de s’en servir.
On pourrait alors aboutir à une méthode du genre :
public void readData() { try { InputStream fis = openFileInput(FILENAME); BufferedReader r = new BufferedReader(new InputStreamReader(fis)); String line; while ((line = r.readLine()) != null) { data.add(line+"\n"); } fis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
Utiliser un stockage externe
Tout d’abord, avant de pouvoir réaliser un accès en lecture/écriture sur la carte SD, il faut que l’application est la permission de le faire, il faut donc ajouter dans le manifest, la demande de permission de lecture : READ_EXTERNAL_STORAGE ou d’écriture : WRITE_EXTERNAL_STORAGE. Toutefois, si vous ajoutez la requête d’accès en écriture, implicitement la lecture sera demandé, car elle est nécessaire pour l’accès en écriture.
<manifest ... <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE " /> ... </manifest>
Bien sur, comme vos données seront sur une carte externe, il n’y a aucune garantie que les fichiers que vous allez enregistrer ne seront pas modifiées. De plus, toutes les applications auront accès en lecture et écriture aux fichiers placés sur la carte, bien sur enfin l’utilisateur pourra modifier ou supprimer vos fichiers à sa guise.
Vérifier la disponibilité de la carte SD
Une bonne pratique avant de réaliser l’écriture sur un fichier externe, c’est voir si ce dernier est bien disponible. Pour ce faire on fait appel à la fonction getExternalStorageState(), elle permet de retourner les différents états que l’on désire vérifier, cela permet par exemple dans le cas où la carte a été retiré, ou encore si elle a été endommagé de notifier l’utilisateur de ce problème. La documentation de google, propose quelques fonctions pour faire la vérification. La première permet de vérifier si le média est disponible en écriture.
public boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return true; } return false; }
Et la seconde, seulement en lecture :
public boolean isExternalStorageReadable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { return true; } return false; }
Ce qui nous donne notre méthode pour écrire sur une carte SD :
if(isExternalStorageWritable()) { String filename = "filename.txt"; File file = new File(Environment.getExternalStorageDirectory(), filename); FileOutputStream fos; try { fos = new FileOutputStream(file); fos.write(new String("Write on SD Card").getBytes()); fos.close(); } catch (FileNotFoundException e) { System.out.println(e); } catch (IOException e) { System.out.println(e); } } else { CharSequence text = "Votre carte SD n'est pas disponible à l'écriture !"; int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(this, text, duration); toast.show(); }
Un petit mémo qui utilise l’écriture sur un fichier
Nous allons réaliser une application qui va être capable d’utiliser des fichiers internes afin d’enregistrer la saisie de l’utilisateur. Voici un aperçu du résultat final :
Le code fonctionnel de l’application mémo
Et sans plus attendre, voici le code source de l’activité correspondante :
public class MainActivity extends Activity { private LinkedList<String> data = new LinkedList<String>(); private String FILENAME = "memo"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button save = (Button) findViewById(R.id.save); save.setOnClickListener(saveListener); Button reset = (Button) findViewById(R.id.reset); reset.setOnClickListener(resetListener); TextView res = (TextView) findViewById(R.id.res); readData(); String text = new String(); for (String str : data) { text += str; } res.setText(text+"\n"); } OnClickListener saveListener = new OnClickListener() { @Override public void onClick(View v) { EditText input = (EditText) findViewById(R.id.input); data.add(new String(input.getText().toString()+"\n")); saveData(); input.setText(""); String text = new String(); TextView res = (TextView) findViewById(R.id.res); for (String str : data) { text += str; } res.setText(text); } }; OnClickListener resetListener = new OnClickListener() { @Override public void onClick(View v) { data.clear(); saveData(); TextView res = (TextView) findViewById(R.id.res); res.setText(""); } }; public void readData() { try { InputStream fis = openFileInput(FILENAME); BufferedReader r = new BufferedReader(new InputStreamReader(fis)); String line; while ((line = r.readLine()) != null) { data.add(line+"\n"); } fis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public void saveData() { FileOutputStream fos; try { fos = openFileOutput(FILENAME, Context.MODE_PRIVATE); for (String str : data) { fos.write(str.getBytes()); } fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
Voyons dans les grandes lignes ce qu’on a :
- Une liste chaînée de String afin de contenir l’ensemble des données ajoutées par l’utilisateur.
- A chaque fois qu’on clique sur le bouton Save, on ajoute la saisie de l’utilisateur à la liste et on sauvegarde l’ensemble de la liste sur un fichier
- Au chargement de l’application, si du contenu existe, on remplit la liste chaînée grâce à la fonction readData.
- On peut reset le contenu du fichier grâce à un bouton prévu à cet effet.
Layout de notre application mémo
Et voici le layout utilisé :
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.file.MainActivity" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignLeft="@+id/textView1" android:layout_alignParentRight="true" android:orientation="vertical" > <EditText android:id="@+id/input" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:text=" " /> <Button android:id="@+id/save" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Save" /> <TextView android:id="@+id/res" android:layout_width="match_parent" android:layout_height="270dp" android:text=" " /> <Button android:id="@+id/reset" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Reset" /> </LinearLayout> </RelativeLayout>
Et voilà, c’est tout cela suffit pour faire une application simple capable de faire des accès écriture/lecture et ainsi d’enregistrer vos données.
Bonjour !
En premier lieu, bel article 🙂 J’ai une remarque concernant l’usage des singletons.
Le pattern singleton permet de résoudre un certain nombre de problème dans une application Java classique mais dans le cadre d’une application Android, c’est assez risqué. En fait l’instance d’un singleton est créée lorsque que la classe est utilisée pour la première fois (ou plus précisément lorsque le class loader charge la classe pour la première fois). Dans une application traditionnelle (type application sur PC) lorsqu’elle « quitte » la JVM s’arrête, les classes précédemment chargées disparaissent et donc les singletons instances également. Ce qui correspond au comportement attendu.
Dans le cas d’une application Android, la vie du processus et de la JVM d’une application est controlée par le framework, ce n’est pas parce que l’application est « arrêtée » (cad on est passé par les méthodes onDestroy() de toutes les instances de Service, Application et Activity) que le class loader est détruit. Ce qui signifie que d’une instance à l’autre de l’application, les singletons peuvent conserver leur valeur. Si cet objet singleton contient un état, vous pouvez de manière non prédictive conserver un état d’une instance à l’autre d’application.
Voilà, c’était juste une petite précision sur la prudence à avoir en utilisant ces singletons 🙂