Appli Android Pour Enquête de Terrain

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 39

Appli Android pour enquête de terrain

posté dans Android, Création Entreprise, Java, Tutoriel écrit le 1 avril 2011 par Nicolas
PARTAGER

Objectifs du tutoriel : Développer une appli Android pour réaliser une enquête de
terrain. L’appli comprend la présentation de questions (avec choix de la langue), la
saisie des réponses des personnes interrogées, le stockage dans une base de donnée,
le comptage du nombre d’enregistrements.

Liste de compétences android développées dans le tutoriel :

 Lancer plusieurs « Activity » (en échangeant des données), les « Intent ».


 Accéder à des ressources externes (fichiers xml).
 Utiliser une base de données sqlite3.

Avis aux entrepreneurs (ou futurs entrepreneurs), ce tuto est pour vous ! ;-)!

Voilà le but ultime du tutoriel : déployer l’application sur une tablette Android et aller
à l’assaut des centres commerciaux pour étudier un marché ;-). Voici l’appli sur ma
tablette Android :
Niveau : Moyen

Pré-requis :

 Avoir un environnement Java sur sa machine.


 Avoir installé le SDK Android (suivez le tutoriel
officiel:http://developer.android.com/sdk/installing.html).
 Avoir une version de l’IDE Eclipse qui gère Java (« Eclipse Java » par
exemple):http://www.eclipse.org/downloads/
 Avoir ajouté le plugin ADT à
Eclipse: http://developer.android.com/sdk/installing.html
 Des connaissances en Java et Android sont souhaitables.
 Posséder un terminal Android pour pouvoir réaliser l’enquête, sinon pour le
développement, l’émulateur AVD (Android Virtual Device) suffira…

Astuce pour les entrepreneurs qui ne sont pas à l’aise en informatique :

 Les données qui définissent le questionnaire de l’enquête sont définies dans un seul
fichier au format XML. Sans connaissance, il vous est très facile de modifier mon
fichier pour l’adapter à votre besoin.
 Ensuite il vous faudra trouver un ami un peu plus à l’aise pour compiler et déployer
l’appli sur un terminal Android.
 N’hésitez pas à me poser des questions !

Ressources : Vous pouvez télécharger le code source de l’intégralité du tutoriel sous


la forme d’un projet Eclipse. Il vous faudra importer le projet plutôt que d’en créer un
nouveau. Vous n’avez qu’à compiler et exécuter l’application!

Code source
Sommaire [Masquer]
 1 Introduction
 2 Spécification de notre besoin
 3 Conception
 4 Création d’un projet sous Eclipse
 5 Layouts
 6 Autres ressources
 7 Code Java de l’application
o 7.1 Partie GUI (Graphical User Interface) de l’appli
 7.1.1 Activité principale
 7.1.2 Activité secondaire
o 7.2 Partie métier
 7.2.1 Les questions
 7.2.2 Le stockage des réponses
 7.2.3 L’exécution des questions
o 7.3 Le fonctionnement
o 7.4 La récupération des données
 8 Conclusion
Introduction
Pour ce tutoriel, je vous propose du concret !

L’appli détaillée dans ce tutoriel est très inspirée de mon appli réelle que je vais
utiliser pour réaliser une enquête de terrain pour mon étude de marché micro-
économique.

Je travaille actuellement sur un projet de création d’entreprise et je me suis dis que le


plus simple pour mon besoin était de saisir les réponses « en live » sur une tablette
Android. J’ai en effet besoin de montrer aux personnes interrogées une présentation
vidéo avant de procéder au questions/réponses. Du coup, je mutualise tout sur ma
tablette : présentation + lecture des questions + saisie des réponses + export d’une
base de données prête à être dépouillée !

En espérant que ceci pourra vous être utile…

Spécification de notre besoin


La spécification du besoin est de :

 Proposer 2 langues : français et anglais.


 Afficher simultanément : une question et les réponses possibles, ceci sur un
maximum de surface de l’écran. Les questions s’enchaineront les unes après les
autres mais ne seront pas affichées en même temps.
 Une seule réponse possible à chaque fois parmi une liste de possibilité. Pas de choix
multiple, pas de réponse ouverte.
 L’application ne gèrera pas la rotation, on forcera donc toujours le mode paysage.

Voici un aperçu de ce que nous allons obtenir :


Conception
La conception est relativement simple.

Voici les classes que nous allons implémenter :

 Question
 Cette classe va contenir les données relatives à une question :
 l’énoncé
 les réponses possibles
 l’orentation du radiobox (les réponses pourront s’enchaîner de manière verticale dans
le cas de textes, ou bien de manière horizontale dans le cas de notes). Un exemple des
2 possibilités :
 Verticalement :
 Oui
 Non
 Je ne sais pas
 Horizontalement :
 12345678
 QuestionFactory
 Cette classe est une factory statique (pas besoin donc de l’instancier). Elle a pour
responsabilité de construire un jeu de questions suivant la langue désirée. Elle va
aller parser le fichier de ressources « strings.xml » afin de récupérer le contenu utile.
Nous verrons plus tard comment définir ce fichier de ressources.
 Pour optimiser, la construction ne devrait se faire qu’une seule fois pour chaque
langue. Dans notre pratique, l’appli ne consommera rien comme ressource de calcul
(pas de 3D, pas de gps, pas de 3G, etc…), nous reconstruirons le jeu de question à
chaque fois. À voir si le nombre de questions devient trop conséquent, il faudra
songer à pré-calculer et à stocker…
 AnswersDbAdapter
 Cette classe va nous permettre de manipuler une base de données au format sqlite3.
C’est le format utilisé sur android. Ce format ressemble énormément à d’autres
systèmes relationnels de base de données.
 Elle fabriquera des requêtes SQL et les exécutera.
 SurveyManager
 C’est l’activité android (Activity) principale. Pour faire simple, c’est la fenêtre
graphique qui sera lancée au démarrage de l’appli. Elle ira faire l’inflation du calque
xml de définition de l’interface homme/machine. L’action d’inflation est simplement
le parsing et le stockage de données, rien de bien méchant…
 Survey
 C’est l’activité android secondaire. Elle naît sur demande de l’activité principale et
meurt quand on arrive au terme du questionnaire. Elle doit récupérer une
information importante de l’activité principale : la langue choisie ! Quand elle meurt,
elle fournira à l’activité principale le nombre de personnes déjà interrogées.
Nous avons vu les classes, mais une application android n’est pas uniquement bâtie
avec des classes Java. On peut également utiliser des ressources (images, fichiers xml,
etc).

Nous allons donc définir 2 layouts xml pour définir l’aspect graphique des 2 activités.
Nous allons y positionner des widgets.

Nous allons enfin définir le fichier strings.xml qui contiendra tout le questionnaire.

Création d’un projet sous Eclipse


Commençons par créer un projet Android sous Eclipse : File / New / Android Project

Il faut choisir un certains nombres de noms : nom du projet Eclipse nom de votre
package Java de base, nom de l’activité principale (titre notre unique fenêtre
graphique). Ensuite il nous faut choisir notre cible. Nous prendrons Android 1.5 car
nous n’aurons pas besoin d’API très spécifiques développées récemment… Le niveau
correspondant (API Level) est le niveau 3.
Voici alors l’arborescence du projet :

Bon, fini de rigoler, commençons!

Layouts
Avant de coder comme des bourrins, commençons par définir notre GUI (interface
graphique). Nous allons faire simple, donc pas ultra beau… Le but est une application
qui répond à notre besoin et de minimiser le temps de développement. Enfin pour
moi c’est vital car il me reste encore quelques bricoles à terminer pour monter mon
entreprise : finir mon étude de marché macro-économique, validation du formulaire,
enquête de terrain, élaboration du business plan, aller à la chasse aux financements,
monter des dossiers, etc, etc,… bon je m’égare, ce n’est pas votre problème ;-).
Voici notre premier layout : survey_nanager.xml

ATTENTION !!! Pour m’être fait xxxxx (comprenez ce que vous voudrez!) plusieurs
fois, pas de majuscule dans le nom du layout ! Sinon le run-time plante lors de
l’inflation mais sans vous donner la moindre explication…

1 <?xml version="1.0" encoding="utf-8"?>


2
3 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id
4
5 <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" andr
6
7 <TextView android:text="Enquête de terrain" android:id="@+id/title" android:layo
8
9 <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" an
10
<TableLayout android:layout_height="wrap_content" android:layout_width="fill_paren
11 <TableRow android:id="@+id/tableRow1" android:layout_width="fill_parent" an
12 <Button android:text="Formulaire \n français" android:id="@+id/frenchForm
13android:layout_column="0" />
14 <Button android:text="English \n form" android:id="@+id/englishForm" an
android:layout_column="1" />
15 </TableRow>
16 </TableLayout>
17
18 <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" a
19
20 <TableLayout android:layout_height="wrap_content" android:layout_width="fill_paren
21 <TableRow android:layout_width="fill_parent" android:layout_height="wrap_con
<TextView android:text="Nombre d\'échantillons : " android:id="@+id/tit
22android:textSize="15sp" />
23 <TextView android:text="X" android:id="@+id/nbSamples" android:layout_width
24 </TableRow>
25 </TableLayout>
26
27 </LinearLayout>
28
</ScrollView>
29

J’ai utilisé :

 des widgets TextView pour afficher du texte (modifiable bien sur)


 des LinearLayout pour agréger plusieurs widgets
 un TableLayout pour avoir 2 bouttons au même niveau qui occupent à eux 2 la
largeur entière
 le tout est wrappé dans un ScrollView pour pouvoir scroller si l’écran est trop petit (ce
n’est en principe pas le cas sur une tablette en mode paysage)
Quelques remarques au passage :

 Pour le retour à la ligne dans une chaîne de caractères, on utilise : \n


 Pour utiliser une apostrophe, on utilise : \’

Je n’ai pas trop creusé ces mises en pages android xml, alors ce layout est largement
critiquable. J’ai même utilisé un LinearLayout pour introduire un espace vertical…
C’est moche… Mais comme mon temps est limité, je ne suis pas allé plus loin sur cet
aspect. N’hésitez pas à me donner des conseils via les commentaires du blog ! Je suis
preneur !

Pour le deuxième layout, on va faire une mise en page pour une question/réponses
seulement. On réutilisera toujours cette structure pour toutes les questions.

Voici : survey.xml

1
2 <?xml version="1.0" encoding="utf-8"?>
3
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id
4
5 <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" andro
6
7 <TextView android:text="" android:id="@+id/question" android:layout_width="wrap_
8
9 <LinearLayout android:orientation="vertical" android:layout_width="fill_parent"
10
11 <RadioGroup android:id="@+id/radioGroup1" android:layout_height="wrap_content" and
12
13 <LinearLayout android:orientation="vertical" android:layout_width="fill_parent"
14
<Button android:text="Valider" android:id="@+id/validQuestion" android:layout_widt
15
16 </LinearLayout>
17
18</ScrollView>
19

Rien à ajouter, cette étape reste assez simple à comprendre. Notez bien que chaque
balise xml peut être munie d’un nom et d’un identifiant. Ce sont ces 2 éléments qui
permettent de récupérer les données ressources à partir du Java.

Autres ressources
Bon nous n’allons pas ajouter d’images, ni d’autres fichiers. Nous allons juste ajouter
des chaînes de caractères dans le fichier strings.xml se situant dans /res/values/

Ce fichier est déjà existant, vous n’avez pas à le créer car il contient déjà le nom de
l’application.

Ici, j’ai choisi une structuration des données que vous pourrez modifier. On aurait pu
définir d’abord toutes les données françaises, puis toutes les anglaises. J’ai
personnellement préféré tout entrelacer pour ne pas oublier de modifier l’anglais si je
décide de retoucher le français. Mais si on ajoutais d’autres langues, ça deviendrait
vite fouillis…

J’ai utilisé des blocs « string » pour des phrases et des blocs « string-array » pour des
tableaux de phrases. J’ai également utilisé un « string » pour définir l’orientation du
bloc de réponse (vertical ou horizontal).

Voici : strings.xml

1 <?xml version="1.0" encoding="utf-8"?>


2 <resources>
<string name="app_name">Enquête de Terrain</string>
3
4 <string name="fr_question1">Quelle est votre nationalité ?</string>
5 <string name="en_question1">What is your nationality ?</string>
6 <string name="orientation1">vertical</string>
7 <string-array name="fr_answer1">
8 <item>Française</item>
<item>Anglaise</item>
9 <item>Autre</item>
10 </string-array>
11 <string-array name="en_answer1">
12 <item>French</item>
<item>English</item>
13 <item>Other</item>
14 </string-array>
15
16 <string name="fr_question2">Vous êtes :</string>
17 <string name="en_question2">You are :</string>
18 <string name="orientation2">vertical</string>
<string-array name="fr_answer2">
19 <item>Homme</item>
20 <item>Femme</item>
21 </string-array>
22 <string-array name="en_answer2">
<item>Male</item>
23
<item>Female</item>
24 </string-array>
25
26 <string name="fr_question3">Trouvez-vous ce produit utile ? Notez de 1 à 8. (1
27 <string name="en_question3">Do you find this product useful ? Grade from 1 to 8
28 <string name="orientation3">horizontal</string>
29 <string-array name="fr_answer3">
<item>1</item>
30 <item>2</item>
31 <item>3</item>
32 <item>4</item>
33 <item>5</item>
<item>6</item>
34 <item>7</item>
35 <item>8</item>
36 </string-array>
37 <string-array name="en_answer3">
38 <item>1</item>
<item>2</item>
39 <item>3</item>
40 <item>4</item>
41 <item>5</item>
42 <item>6</item>
<item>7</item>
43 <item>8</item>
44 </string-array>
45
46 </resources>
47
48
49
50
51
52
53
54
55

Je pense que ce code est assez compréhensible, je passe donc… Il vous tarde de coder,
je le sens…

Ah j’ai oublié un détail, tant que nous sommes dans le xml… Nous devons forcer
l’application en mode « landscape » pour ne pas avoir à gérer la rotation (car ce n’est
pas l’objet de ce tutoriel et ce serait superflu pour notre besoin). Pour cela rendez-
vous dans le manifest de l’appli : AndroidManifest.xml à la racine du projet.

Il faut ajouter

android:screenOrientation="landscape"

dans la seule balise

activity

que nous avons pour l’instant. Le résultat ressemble à un truc comme ceci :
&lt;activity android:name=".SurveyManager"
android:screenOrientation="landscape" android:label="@string/app_name"&gt;

Code Java de l’application


Ok, nous y voilà. Nous allons coder progressivement. Pas question de tenter de coder
les 2 activités directement tout en ouvrant la base de données… Ne soyons pas

fous

Partie GUI (Graphical User Interface) de l’appli

Nous allons commencer par l’activité principale.

Activité principale

Normalement, en créant le projet, Eclipse vous a crée une classe. La mienne


s’appellera SurveyManager. Si vous souhaitez modifier ce nom, rien de plus simple,
vous sélectionnez ce nom de classe dans votre fichier java, puis vous allez dans le
menu « Refactor » puis « Rename ». Il vous suffit de modifier le nom et de taper sur
« entrée » de votre clavier. Ainsi toutes les occurrences dans tout le projet ont changé
de nom! Pour preuve, regardez votre manifest… Eclipse ça envoi :-)!

Dans un premier temps, nous allons procéder à l’inflation du layout (c’est à dire
récupérer tous les identifiants des widgets définis dans surveyManager.xml).
L’inflation va créer une classe R qui nous permettra très facilement de récupérer des
références Java sur ces widgets. Magique :-)!

Ensuite nous allons ajouter des écouteurs afin de capter les évènements « simple
clic » sur nos 2 boutons. Pour l’instant nous ne ferons rien dans la méthode de
callback de notre écouteur. Plus tard nous lancerons une nouvelle activité android…

Voici donc le code de SurveyManager.java à ce stade :

1 package com.codeWeblog;
2
3 import android.app.Activity;
4 import android.content.Intent;
import android.os.Bundle;
5 import android.view.View;
6 import android.view.View.OnClickListener;
7 import android.widget.Button;
8 import android.widget.TextView;
9
10 /**
11 * This is our main class. This is our main Android activity
* which be first launched.
12 *
13 * @author nvergnes
14 *
15 */
public class SurveyManager extends Activity {
16
17 /**
18 * Constant : id used for French survey activity
19 */
20 public static final int ACTIVITY_SURVEY_FR = 1;
21
22 /**
* Constant : id used for English survey activity
23 */
24 public static final int ACTIVITY_SURVEY_EN = 2;
25
26 /**
27 * Reference on a button
*/
28 Button mFrenchForm;
29
30 /**
31 * Reference on a button
32 */
33 Button mEnglishForm;
34
/**
35 * Reference on a text widget : to display the number of records
36 */
37 TextView mNbSamplesDisplay;
38
39 /**
40 * Number of samples
*/
41 int mNbSamples;
42
43 /**
44 * Called when the activity is first created.
45 */
@Override
46 public void onCreate( Bundle savedInstanceState ) {
47 super.onCreate( savedInstanceState );
48
49 // xml layout inflation
50 setContentView( R.layout.survey_manager );
51
52 // We take references on graphical widgets
mFrenchForm = ( Button )findViewById( R.id.frenchForm );
53 mEnglishForm = ( Button )findViewById( R.id.englishForm );
54 mNbSamplesDisplay = ( TextView )findViewById( R.id.nbSamples );
55
56 // We set listener on French survey launcher
57 mFrenchForm.setOnClickListener( new OnClickListener() {
@Override
58 public void onClick( View view ) {
59 // noting for now...
60 // TODO Launch a new activity
}
61 });
62
63 // We set listener on English survey launcher
64 mEnglishForm.setOnClickListener( new OnClickListener() {
65 @Override
public void onClick( View view ) {
66 // noting for now...
67 // TODO Launch a new activity
68 }
69 });
70 }
}
71
72
73
74
75
76
77
78
79
80
81
82
83

Lançons notre appli avec l’émulateur AVD… Super, voilà notre première activité!

Bon pour l’instant c’est un peu en bois tout de même… Rien ne se passe quand on
clique sur les boutons.
Activité secondaire

Avant de passer au code « métier », c’est à dire l’enregistrement des réponses, nous
allons terminer toute la partie GUI.

Passons donc à notre activité secondaire dont nous avons déjà regardé le
layout survey.xml.

Pour l’instant bâtissons quelque chose de simple dans un nouveau fichier


: Survey.java. Nous allons juste appeler le mécanisme inflation, récupérer quelques
références sur des widgets et ajouter un écouteur sur le changement d’état du widget
RadioGroup. Ainsi, dès que l’utilisateur cochera une case, nous pourrons lancer une
méthode spécifique ou tout simplement noter le résultat.

Voici un premier fichier avant de passer au mécanisme des intentions :

package com.codeWeblog;
1
2 import android.app.Activity;
3 import android.os.Bundle;
4 import android.widget.Button;
5 import android.widget.RadioGroup;
import android.widget.TextView;
6
7
/**
8 * This is our secondary activity, called by SurveyManager activity. Its responsi
9 * is to ask all questions, and store the result, at last, in a database. Then, t
10 * activity is focused again.
11 *
* @author nvergnes
12 *
13 */
14 public class Survey extends Activity implements RadioGroup.OnCheckedChangeListener
15
16 /**
17 * Constant : the number of questions in the survey
*/
18 public static final int NUMBER_OF_QUESTIONS = 3;
19
20 /**
21 * All the answers of one person
22 */
int[] mAnswers = new int[ NUMBER_OF_QUESTIONS ];
23
24 /**
25 * Button to valid questions
26 */
27 Button mValidQuestion;
28
29 /**
* Possibles answers for each question
30 */
31 String [] mItemsQuestion;
32
33 /**
* Current question
34 */
35 int mIndexCurrentQuestion = 1;
36
37 /**
38 * Number of samples
*/
39 int mNbSamples;
40
41 /**
42 * RadioGroup
43 */
44 RadioGroup mRadioGroup;
45
/**
46 * The text of the question
47 */
48 TextView mQuestion;
49
50 /**
* Id of the the checked radio
51 */
52 int mCheckedIndex = -1;
53
54 /**
55 * Language used in survey
56 */
int mLang;
57
58 /**
59 * Called when the activity is first created.
60 */
61 @Override
62 public void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
63
64 // Xml layout inflation
65 setContentView( R.layout.survey );
66
67 // We get a reference on the radio-group widget
68 mRadioGroup = ( RadioGroup )findViewById( R.id.radioGroup1 );
mRadioGroup.setOnCheckedChangeListener( this );
69
70 // We get a reference on the text-view widget
71 mQuestion = ( TextView )findViewById( R.id.question );
72
73 }
74
75 /**
76 * This method is called as soon as a radio button is clicked
*/
77 @Override
78 public void onCheckedChanged( RadioGroup radioGroup, int checkedId ) {
79 // We remember the id of the checked radio
80 mCheckedIndex = -1;
81
82 // We check all radio button value
83 for ( int i = 0; i < radioGroup.getChildCount(); i++ ) {
if ( radioGroup.getChildAt( i ).getId() == checkedId ) {
84 mCheckedIndex = i;
85 break;
86 }
87 }
}
88 }
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

Reste ensuite à déclarer cette nouvelle activité au sein du manifest. Le bloc qui suit
doit être ajouté dans le bloc xml de l’application :

1 <activity android:name=".Survey" android:screenOrientation="landscape" android:label=


2 <intent-filter>
3 <action android:name="android.intent.action.DEFAULT" />
4 </intent-filter>
</activity>
5

Ok. Maintenant, nous devons lancer cette activité à partir de la première que nous
avons créée.

Nous allons donc devoir lancer une intention depuis notre première activité. Nous
n’allons pas laisser le choix au système android de chercher avec quel appli il pourrait
répondre à cette intention. À la place, nous allons tout simplement utiliser la
deuxième activité pour répondre à cette intention.

On peut attacher des données dans un intention afin de faire communiquer 2


activités. On appelle « bundle » cet agrégat de données.

Voici les modifications pour la première activité dans SurveyManager.java :


1
2
3
4 /**
5 * Called when the activity is first created.
6 */
@Override
7 public void onCreate( Bundle savedInstanceState ) {
8 super.onCreate( savedInstanceState );
9
10 // xml layout inflation
11 setContentView( R.layout.survey_manager );
12
13 // We take references on graphical widgets
mFrenchForm = ( Button )findViewById( R.id.frenchForm );
14 mEnglishForm = ( Button )findViewById( R.id.englishForm );
15 mNbSamplesDisplay = ( TextView )findViewById( R.id.nbSamples );
16
17 // We set listener on French survey launcher
18 mFrenchForm.setOnClickListener( new OnClickListener() {
19 @Override
public void onClick( View view ) {
20 // We define a new intent in order to launch a new activity
21 Intent intent = new Intent( SurveyManager.this, Survey.class );
22
23 // We put the language setup in this intent
24 intent.putExtra( "lang", ACTIVITY_SURVEY_FR );
25
// We launch a new activity
26 startActivityForResult( intent, ACTIVITY_SURVEY_FR );
27 }
28 });
29
30 // We set listener on English survey launcher
31 mEnglishForm.setOnClickListener( new OnClickListener() {
@Override
32 public void onClick( View view ) {
33 // We define a new intent in order to launch a new activity
34 Intent intent = new Intent( SurveyManager.this, Survey.class );
35
36 // We put the language setup in this intent
intent.putExtra( "lang", ACTIVITY_SURVEY_EN );
37
38 // We launch a new activity
39 startActivityForResult( intent, ACTIVITY_SURVEY_EN );
40 }
41 });
42 }
43
44
45

Quelques explications s’imposent…

Intent intent = new Intent( SurveyManager.this, Survey.class );


permet de créer une intention et d’y répondre explicitement avec la classe Java
Survey.

intent.putExtra( "lang", ACTIVITY_SURVEY_FR );

permet de placer une donnée dans l’activité, dans un bundle. Ici il s’agit d’un code
que j’ai défini. Je me suis défini 2 constantes une pour les questions françaises, une
pour les questions anglaises.

startActivityForResult( intent, ACTIVITY_SURVEY_FR );

permet de lancer la nouvelle activité à partir de notre intention. Ici on doit passer un
entier pour coder cet appel à la nouvelle activité. J’ai réutilisé
ACTIVITY_SURVEY_FR mais c’est purement arbitraire, j’aurais pu passer n’importe
quel entier…

Pour l’écouteur sur l’autre bouton, c’est exactement le même code, je ne m’étend donc
pas.

Nous devons maintenant ajouter une nouvelle méthode à notre classe


: onActivityResult. Cette méthode va définir le comportement que doit adopter notre
activité principale lorsqu’elle reprendra la main, c’est à dire quand l’activité
secondaire sera terminée. Voici le code :

1 /**
2 * Called when the child activity is closed, we must give focus on
3 * the initial activity : this one!
*/
4 @Override
5 protected void onActivityResult( int requestCode, int resultCode, Intent intent )
6 super.onActivityResult( requestCode, resultCode, intent );
7
8 // We get the bundle from the Survey activity
9 Bundle extras = intent.getExtras();
10
switch( requestCode ) {
11 case ACTIVITY_SURVEY_FR:
12 case ACTIVITY_SURVEY_EN:
13 mNbSamples = extras.getInt( "nbSamples" );
14 mNbSamplesDisplay.setText( Integer.toString( mNbSamples ) );
break;
15
16
default:
17 // We do nothing
18 break;
19 }
20 }
21
22
23

Cette méthode récupère le bundle crée par l’activité secondaire (ce bundle ayant été
encapsulé dans l’intention de « retour »). Ensuite elle analyse le code de retour. Ce
code correspond au code passé en argument dans :

startActivityForResult( intent, ACTIVITY_SURVEY_FR );

On peut ainsi savoir de quelle activité on arrive.

On est OK pour cette activité. Allons donc dans l’activité secondaire : Survey.java

1
2
/**
3 * Called when the activity is first created.
4 */
5 @Override
6 public void onCreate( Bundle savedInstanceState ) {
7 super.onCreate( savedInstanceState );
8
// We get the intent to know if the survey is in French or English
9 Bundle extras = getIntent().getExtras();
10 if ( extras != null ) {
11 mLang = extras.getInt( "lang" );
12 }
13
// Xml layout inflation
14 setContentView( R.layout.survey );
15
16 // We get a reference on the radio-group widget
17 mRadioGroup = ( RadioGroup )findViewById( R.id.radioGroup1 );
18 mRadioGroup.setOnCheckedChangeListener( this );
19
20 // We get a reference on the text-view widget
mQuestion = ( TextView )findViewById( R.id.question );
21
22 }
23
24

Nous avons donc juste ajouté le mécanisme de récupération de l’intention en vue de


décortiquer le bundle de données. On récupère et on mémorise la langue choisie dans
un membre d’instance.

Bon il va falloir attaquer les choses moins visuelles maintenant…

Partie métier
Les questions

Les questions vont être construites avec une fabrique (design pattern factory), ceci à
partir du fichier de ressource strings.xml que nous avons défini.

La classe Question.java nous permet de se reposer un instant, c’est uniquement une


classe de stockage. Rien à comprendre ici.

1 package com.codeWeblog;
2
3 import android.widget.RadioGroup;
4
5 /**
* This class contains a question :
6 * - text (subject)
7 * - possibles answers
8 * - orientation (graphical widget)
9 *
* @author nvergnes
10 *
11 */
12 public class Question {
13 /**
14 * Question formulation
*/
15 String mText;
16
17 /**
18 * Possibles answers for this question
19 */
String [] mPossibleAnswers;
20
21 /**
22 * Orientation of the list which contains all possible answers
23 */
24 int mOrientation = RadioGroup.HORIZONTAL;
25
26 /**
* Setter
27 * @param text
28 */
29 void setText( String text ) {
30 mText = text;
}
31
32
/**
33 * Setter
34 * @param possibleAnswers
35 */
36 void setPossibleAnswers( String [] possibleAnswers ) {
mPossibleAnswers = possibleAnswers;
37 }
38
39 /**
40 * Setter
41 * @param orientation
42 */
43 void setOrientation( int orientation ) {
mOrientation = orientation;
44 }
45
46 /**
47 * Getter
48 * @return Orientation
*/
49 int getOrientation() {
50 return mOrientation;
51 }
52
53 /**
54 * Getter
* @return Text
55 */
56 String getText() {
57 return mText;
58 }
59
/**
60 * Getter
61 * @return Possible answers
62 */
63 String [] getPossibleAnswers() {
64 return mPossibleAnswers;
}
65
66 }
67
68
69
70
71
72
73
74
75
76
77
78

Pour la classe QuestionFactory.java les choses se corsent un peu :

1 package com.codeWeblog;
2
3 import java.util.ArrayList;
4
5 import android.content.Context;
import android.widget.RadioGroup;
6
7 public class QuestionFactory {
8 /**
9 * Factory : This static method creates all the questions/responses from the xml
10 *
11 * @param context The context to access the resources
12 * @param numberOfQuestions Number of questions
* @return The set of questions
13 */
14 public static ArrayList<Question> createQuestions( Context context, int numberOfQue
15
16 // Construction of a set of questions
17 ArrayList<Question> questions = new ArrayList<Question>( numberOfQuestions )
18
// Parameter chosen with the language parameter
19 String question = new String( "" );
20 String answer = new String( "" );
21
22 // We adapt our construction with the language parameter
23 switch ( language ) {
24 case SurveyManager.ACTIVITY_SURVEY_FR:
question += "fr_question";
25 answer += "fr_answer";
26 break;
27
28 case SurveyManager.ACTIVITY_SURVEY_EN:
29 question += "en_question";
answer += "en_answer";
30 break;
31
32 default:
33 // impossible case
34 break;
35 }
36
// We parse the xml resources for each questions
37 for ( int i = 0; i < numberOfQuestions; i++ ) {
38
39 // To store a temp id
40 int id;
41
42 // The new question build by the factory
43 Question currentQuestion = new Question();
44
// We build an integer id from the name of the resource
45 id = context.getResources().getIdentifier( question + Integer.toString( i
46 // We get the resource in the xml file
47 currentQuestion.setText( context.getResources().getString( id ) );
48
49 // We build an integer id from the name of the resource
id = context.getResources().getIdentifier( answer + Integer.toString( i +
50 currentQuestion.setPossibleAnswers( context.getResources().getStringArray
51
52 // We build an integer id from the name of the resource
53 id = context.getResources().getIdentifier( "orientation" + Integer.toStrin
54 // We get the resource in the xml file
55 String orientation = context.getResources().getString( id );
56
if ( orientation.equalsIgnoreCase( "vertical" ) ) {
57 currentQuestion.setOrientation( RadioGroup.VERTICAL );
58 }
59 else if ( orientation.equalsIgnoreCase( "horizontal" ) ) {
60 currentQuestion.setOrientation( RadioGroup.HORIZONTAL );
}
61 else {
62 // impossible case
throw new Exception( "Mauvaise saisie dans le fichier string.xml : un
63 }
64
65 // We push a new question in the list
66 questions.add( currentQuestion );
67 }
68
// We return the result
69 return questions;
70 }
71 }
72
73
74
75
76
77
78
79
80
81
82
83

On va boucler sur le nombre de questions. Pour chaque itération, on va récupérer un


« id » android (qui n’est rien d’autre qu’un entier) grâce au nom qui est adjoint à l’id
dans les balises XML. En général, on utilise directement les id, c’est beaucoup plus
performant. Ici, on n’a pas le choix car on souhaite utiliser un objet String auquel on
concatène le numéro de la question. Si on souhaite utiliser directement le getter sur la
ressource avec l’id, on ne peut donc pas faire de boucle… Ce qui est impensable ici :-
(…

Ensuite, ayant cet id, on peut aller chercher la ressource. Attention au type de la
ressource. Il existe lesstring et les string-array. Les retours ne sont pas les mêmes.
Dans le premier cas, on récupère un String alors que dans le second cas, on récupère
un String[].

Enfin on injecte toutes ces données dans des objets Question que l’on agrège dans un
ArrayList. On a alors notre jeu de questions contenant tout : questions, réponses,
orientation du widget des réponses,…

Le stockage des réponses


Bon un fois que tout le mécanisme de fabrication du jeu de questions est ok, il reste à
élaborer un mécanisme de stockage des réponses.

Je ne vais pas trop m’attarder sur ce code car j’ai réutilisé les principes du tutoriel
officiel de google Notepad
: http://developer.android.com/resources/tutorials/notepad/index.html. Ce tutorial
est vraiment excellent et le code source est fourni ICI.

Voici le code, que j’ai tout de même un peu adapté >:-o :

package com.codeWeblog;
1
2 import android.content.Context;
3 import android.database.Cursor;
4 import android.database.sqlite.SQLiteDatabase;
5 import android.database.sqlite.SQLiteException;
6 import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
7
8 /**
9 * This class
10 *
11 * @author nvergnes
*
12 */
13 public class AnswersDbAdapter {
14
15 public static final int DATABASE_VERSION = 2;
16 public static final String DATABASE_NAME = "dataBase";
17 public static final String TABLE_NAME = "answers";
18
/**
19 * Sql query to create a table in the database
20 */
21 private String mSqlCreateTable;
22
23 /**
* Sql query to to add a record in the table of the database
24
*/
25 private String mSqlAddAnswer;
26
27 /**
28 * Reference on database
29 */
private SQLiteDatabase mDataBase;
30
31 /**
32 * Context
33 */
34 private Context mContext;
35
36 /**
* Number of field, equals to the number of questions
37 */
38 private int mNbFields;
39
40 /**
* Constructor
41 *
42 * @param context
43 * @param nbFields
44 */
public AnswersDbAdapter( Context context, int nbFields ) {
45 mContext = context;
46 mNbFields = nbFields;
47
48 // We create the main queries used to create the table and add records
49 buildAddRecordQuery();
50 buildCreateNewTableQuery();
}
51
52 /**
53 * Open the database
54 * @throws SQLiteException
55 */
public void open() throws SQLiteException {
56 mDataBase = ( new DatabaseHelper( mContext, DATABASE_NAME, null, DATABASE_VER
57 }
58
59 /**
60 * Close the database
61 */
public void close() {
62 mDataBase.close();
63 }
64
65 /**
66 * Construct a sql query in order to add a new record
67 * in the database
*/
68 public void buildAddRecordQuery() {
69
70 mSqlAddAnswer = new String( "INSERT INTO " );
71
72 // Beginning of the structure
73 mSqlAddAnswer += TABLE_NAME + " (";
74
// All other fields
75 for ( int i = 0; i < ( mNbFields - 1 ); i++ ) {
76 mSqlAddAnswer += "answer" + Integer.toString( i + 1 ) + "Code" + ", ";
77 }
78 // The end of the structure
79 mSqlAddAnswer += "answer" + Integer.toString( mNbFields ) + "Code" + ") VALUE
80
}
81
82 /**
83 * Construct a sql query in order to create our table
84 * in the database
85 */
public void buildCreateNewTableQuery() {
86
87 mSqlCreateTable = new String( "CREATE TABLE " );
88
89 // Beginning of the structure
90 mSqlCreateTable += TABLE_NAME + " (_id INTEGER PRIMARY KEY AUTOINCREMENT,
91
// All other fields
92 for ( int i = 0; i < ( mNbFields - 1 ); i++ ) {
93 mSqlCreateTable += "answer" + Integer.toString( i + 1 ) + "Code" + " INTE
94 }
95 // The end of the structure
mSqlCreateTable += "answer" + Integer.toString( mNbFields ) + "Code" + " INTE
96 }
97
98 /**
99 * Add a new record in the database : it's a new set of answers.
100 *
101 * @param answers
*/
102 public void createAnswer( int [] answers ) {
103 // sql query initialization
104 String query = new String( mSqlAddAnswer + "( '" );
105
106 // We create the rest of the sql query
for ( int i = 0; i < answers.length-1; i++ ) {
107 query += Integer.toString( answers[ i ] ) + "' , '";
108 }
109 query += Integer.toString( answers[ answers.length - 1 ] ) + "' )";
110
111 // We execute the sql query
112 mDataBase.execSQL( query );
}
113
114 /**
115 * To get the id of the last record in database
116 *
117 * @return The id of the last record in database (it equals to the number of sa
118 */
public int getLastId() {
119 // sql query
120 String query = new String( "select count(*) from answers" );
121
122 // We execute the sql query
123 Cursor result = mDataBase.rawQuery( query, null );
124
// We set the cursor at the beginning
125 result.moveToFirst();
126
127 // We return the result : it's the number of samples
128 return result.getInt( 0 );
129 }
130
131 /**
* Inner class useful to create a new table
132 *
133 * @author nvergnes
134 *
135 */
private class DatabaseHelper extends SQLiteOpenHelper {
136
137 public DatabaseHelper( Context context, String name, CursorFactory factory, i
138 super( context, name, factory, version );
139 }
140
@Override
141 public void onCreate( SQLiteDatabase db ) {
142 db.execSQL( mSqlCreateTable );
143 }
144
145 @Override
public void onUpgrade( SQLiteDatabase arg0, int arg1, int arg2 ) {
146 // Nothing, is done here
147 }
148 }
149
150 }
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

En principe mon code est correctement documenté. Si vous ne comprenez pas tout,
posez moi des questions dans les commentaires du billet.

L’exécution des questions

Il nous reste un dernier morceau à coder : le déroulement des questions. Rappelez-


vous, nous avons placé une seule question (avec ses réponses associées) dans l’activité
secondaire. Il aurait été très … très quoi? … très con en fait, n’ayant pas honte des
mots! Oui donc très con de définir une activité par question, sachant que toutes les
questions se présentent quasi de la même façon.

Voici donc le code final pour l’activité secondaire Survey.java :

1 package com.codeWeblog;
2
3 import java.util.ArrayList;
4
5 import android.app.Activity;
import android.content.Context;
6 import android.content.Intent;
7 import android.database.sqlite.SQLiteException;
8 import android.os.Bundle;
9 import android.view.Gravity;
10 import android.view.View;
import android.view.View.OnClickListener;
11 import android.widget.Button;
12 import android.widget.RadioButton;
13 import android.widget.RadioGroup;
14 import android.widget.TextView;
import android.widget.Toast;
15
16 /**
17 * This is our secondary activity, called by SurveyManager activity. Its responsi
18 * is to ask all questions, and store the result, at last, in a database. Then, t
19 * activity is focused again.
20 *
* @author nvergnes
21 *
22 */
23 public class Survey extends Activity implements RadioGroup.OnCheckedChangeListener
24
25 /**
* Constant : the number of questions in the survey
26
*/
27 public static final int NUMBER_OF_QUESTIONS = 3;
28
29 /**
30 * All the answers of one person
31 */
int[] mAnswers = new int[ NUMBER_OF_QUESTIONS ];
32
33 /**
34 * Button to valid questions
35 */
36 Button mValidQuestion;
37
38 /**
* Possibles answers for each question
39 */
40 String [] mItemsQuestion;
41
42 /**
43 * Current question
44 */
45 int mIndexCurrentQuestion = 1;
46
/**
47 * Database to store results
48 */
49 AnswersDbAdapter mDataBase;
50
51 /**
* Number of samples
52 */
53 int mNbSamples;
54
55 /**
56 * RadioGroup
57 */
RadioGroup mRadioGroup;
58
59 /**
60 * Set of questions (construction with a specific factory)
61 */
62 ArrayList<Question> mQuestions;
63
/**
64 * The text of the question
65 */
66 TextView mQuestion;
67
68 /**
69 * Id of the the checked radio
*/
70 int mCheckedIndex = -1;
71
72 /**
73 * Language used in survey
74 */
75 int mLang;
76
/**
77 * Called when the activity is first created.
78 */
79 @Override
80 public void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
81
82 // We get the intent to know if the survey is in French or English
83 Bundle extras = getIntent().getExtras();
84 if ( extras != null ) {
85 mLang = extras.getInt( "lang" );
86 }
87
// Xml layout inflation
88 setContentView( R.layout.survey );
89
90 // We get a reference on the radio-group widget
91 mRadioGroup = ( RadioGroup )findViewById( R.id.radioGroup1 );
92 mRadioGroup.setOnCheckedChangeListener( this );
93
// We get a reference on the text-view widget
94 mQuestion = ( TextView )findViewById( R.id.question );
95
96 try {
// We construct the set of questions
97 mQuestions = QuestionFactory.createQuestions( this, NUMBER_OF_QUESTIO
98
99 // We create the database access
100 mDataBase = new AnswersDbAdapter( (Context)this, NUMBER_OF_QUESTIONS )
101
102 // We get a reference on the validation widget
mValidQuestion = ( Button )findViewById( R.id.validQuestion );
103 mValidQuestion.setOnClickListener( new OnClickListener() {
104 @Override
105 public void onClick( View view ) {
106
107 // We create, or open the data base
108 mDataBase.open();
109
// We test if the list is empty
110 if ( mCheckedIndex == -1 ) {
111 // We show a toast to say : no answer !
112 Toast.makeText( view.getContext(), "Aucune réponse n\'a été
113 }
// In this case, the list is not empty
114 else {
115 // We show a toast to say : ok, question validated
116 Toast.makeText( view.getContext(), "Question " + Integer.toS
117 Toast.LENGTH_SHORT ).show();
118
119 // We memorize the current answer
mAnswers[ mIndexCurrentQuestion - 1 ] = mCheckedIndex;
120
121 // We go to the next question
122 mIndexCurrentQuestion++;
123
124 // If there is other questions to deal with
125 if ( mIndexCurrentQuestion <= NUMBER_OF_QUESTIONS ) {
126 mQuestion.setText("");
executeQuestion( mIndexCurrentQuestion );
127 }
128
129 // We test if the series of questions are ended
130 if ( mIndexCurrentQuestion == NUMBER_OF_QUESTIONS + 1 ) {
131 // We store all answer in database
mDataBase.createAnswer( mAnswers );
132
133 // We count the number of samples
134 mNbSamples = mDataBase.getLastId();
135
136 // We close the database at the end
137 mDataBase.close();
138
139 // Display with a toast
Toast.makeText( view.getContext(), "Fin du questionnaire
140
141 // We terminate this activity, to go back to the mana
142 // We define a bundle to store the result to send to
143 Bundle bundle = new Bundle();
144
145 // We set the number of samples in the bundle
146 bundle.putInt( "nbSamples", mNbSamples );
147
// We create a new Intent to go back to the first activi
148 Intent intent = new Intent();
149 intent.putExtras( bundle );
150
151 // We set the return status OK
152 setResult( RESULT_OK, intent );
finish();
153 }
154 }
155 }
156
157 });
158
159 // Execute the first question
executeQuestion( mIndexCurrentQuestion );
160
161 }
162 // If there is an exception, for example the database is not usable...
163 catch ( SQLiteException e ) {
164 Toast.makeText( this, "Impossible d'ouvrir la base de donnees", Toast.
}
165 catch ( Exception e ) {
166 Toast.makeText( this, "Problème... L'erreur vient probablement d'un fich
167 }
168 }
169
170 /**
* This method execute one question : display, parameterization,...
171 *
172 * @param indexQuestion Index of the question to execute
173 */
174 public void executeQuestion( int indexQuestion ) {
175 // We initialize again this attribute before each question
mCheckedIndex = -1;
176
177 // We destroy all the radio-group children
178 mRadioGroup.removeViews( 0, mRadioGroup.getChildCount() );
179
180 // We clear old choice
181 mRadioGroup.clearCheck();
182
// Current question
183 Question currentQuestion = mQuestions.get( indexQuestion - 1 );
184
185 // We set a correct display
186 mRadioGroup.setOrientation( currentQuestion.getOrientation() );
187 mRadioGroup.setHorizontalGravity( Gravity.LEFT );
188
189 // We set the text in the widget
mQuestion.setText( currentQuestion.getText() );
190
191 // We get all possible answers
192 mItemsQuestion = currentQuestion.getPossibleAnswers();
193
194 // We create some radio-button dynamically, and set the correct text
195 for ( int i = 0; i < mItemsQuestion.length; i++ ) {
RadioButton radioButton = new RadioButton( this );
196 // We set the text in the widget
197 radioButton.setText( mItemsQuestion[ i ] );
198 mRadioGroup.addView( radioButton );
199 }
}
200
201 /**
202 * This method is called as soon as a radio button is clicked
203 */
204 @Override
205 public void onCheckedChanged( RadioGroup radioGroup, int checkedId ) {
// We remember the id of the checked radio
206 mCheckedIndex = -1;
207
208 // We check all radio button value
209 for ( int i = 0; i < radioGroup.getChildCount(); i++ ) {
210 if ( radioGroup.getChildAt( i ).getId() == checkedId ) {
mCheckedIndex = i;
211 break;
212 }
213 }
214 }
215 }
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251

Quelques explications…

Dans le bloc try de la méthode onCreate, on peut voir :

 L’ouverture de la base de donnée :

mDataBase.open();

 On exécute la question, c’est à dire on met à jour l’affichage et on réinitialise les cases
à cocher.
 On mémorise toutes les réponses dans un tableau d’entiers (

mAnswers

) après chaque question. Mes réponses sont codées de 0 à N-1.

 Quand on est à la dernière question on exporte toutes les valeurs du tableau des
réponses

mAnswers

dans la base de données. On ferme ensuite cette base.

 Enfin on agrège le nombre d’enregistrements dans le bundle encapsulé dans


l’intention afin d’en informer l’activité principale.

Le fonctionnement

Bon l’appli doit maintenant tourner!

Au départ, dans l’activité principale, le nombre d’enregistrements n’est pas connu. On


attendra d’avoir une saisie pour récupérer ce nombre.

Voici le fonctionnement en images :


[Show as slideshow]

La récupération des données

Ok, c’est bien joli de saisir des réponses mais il faut aussi pouvoir tout récupérer pour
dépouiller ces données! Il suffit de récupérer la base dans l’emplacement suivant sur
votre terminal : data/data/nomPackageJava/databases/nomBaseDonnees

Pour ce faite, vous disposer d’un outil intégré dans le SDK Android de google. Cet
outil, c’est ADB (Android Debug Bridge). Eclipse s’appuie énormément sur ADB
même si pour nous tout est transparent. Quand vous allez débuguer votre appli,
n’oubliez pas que vous êtes sur votre PC et l’appli sur un terminal distant (que vous
soyez sur l’émulateur ou sur un terminal réel, c’est pareil dans les 2 cas).

Lancer une invite de commande, et placez vous dans le sdk. Ensuite vous rentrez dans
les répertoires suivants : ./android-sdk-linux_x86/platform-tools

Pour plus d’infos sur adb, consultez la doc officielle : ICI.

Pour lister les terminaux connectés, on entre :

adb devices

On voit dans mon exemple que seulement un émulateur est connecté. On pourrait
lancer plusieurs instances d’émulateurs avec plusieurs versions d’Android par
exemple.

Pour récupérer des données sur l’émulateur (n’oubliez pas, on cherche à récupérer les
données ;-)), on utilise la commande :

adb pull data/data/$PACKAGE/databases/$NOM_BASE $HOME/$WORKSPACE_ECLIPSE

J’ai utilisé des variables d’environnement afin de vous donner une commande
générique. Vous n’avez plus qu’à remplacer les variables par des noms de votre choix.
Par exemple dans le tutoriel, on a utilisé PACKAGE=com.codeWeblog
Édition de la base sqlite3 (j’utilise SQLite Database Browser qui est le soft le plus
simple du monde) :

Il suffit de l’exporter au format csv pour pouvoir dépouiller avec un tableur. Pas mal
non?

Conclusion
Pour conclure, dans ce tutoriel, nous aurons balayé pas mal de notions qui pourront
vous être utile dans un bon nombre d’autres applications. Ces notions sont
principalement : les intentions, les activités, les ressources, les bases sqlite3,… Nous
avons codé une appli de A à Z!

Cette application peut directement servir de base pour la réadapter suivant ces
besoins pour réaliser une enquête de terrain. On peut imaginer les modifications et
améliorations suivantes :

 Construire les questions une seule fois et les stocker.


 Ajouter la possibilité de saisir plusieurs réponses simultanément, auquel cas il faudra
une vraie base de données relationnelle. Dans mon exemple on a juste un table très
basique.
 Faire un design de la partie GUI pour avoir une belle appli.
 …
Je vais personnellement utiliser cette appli dans quelques semaines pour l’étude de
marché pour mon projet de création d’entreprise. Je partagerai cette expérience sur
mon blog.

Bon tuto

Vous aimerez peut-être aussi