I. Introduction▲
Tout d'abord, je ne saurais que trop vous recommander de lire l'article dédié au pattern architectural modèle vue contrôleur rédigé par Baptiste Wicht (accessible ici), duquel je me suis plus que largement inspiré avant de pondre ce tutoriel, qui se veut plus une explication de l'adaptation que j'ai utilisée à l'intuition quant au pattern MVC qu'une règle stricte à employer partout…
Le tutoriel précédemment évoqué, à l'instar de la plupart des tutoriels que vous pourrez trouver sur le sujet, repose sur l'utilisation large et justifiée des évènements et de l'abonnement à des signaux. Le problème se corse rapidement en JavaME puisque comme vous avez dû vous en rendre compte si vous lisez le présent article, la gestion des évènements n'est pas supportée en J2ME. Conclusion : il a fallu trouver une astuce. Appelons cela un contournement et voyons voir ce que j'ai à vous proposer !
II. Les principes de l'astuce MVC en J2ME▲
En théorie, le modèle signale à la vue le moindre des changements qui s'opèrent en lui par des évènements, que la vue surveille dans le but de se modifier en adéquation (la plupart du temps, cela se fait par l'implémentation du pattern Observer, je vous laisse le soin de mener vos propres recherches sur le sujet). Le fait est que dans le fond, tout est gouverné par la partie contrôleur, puisque c'est lui qui contient toute la logique applicative. En d'autres termes, pourquoi, une fois l'ordre de modification transmis au modèle, ce ne serait pas le contrôleur qui dirait à la vue de se modifier plutôt que de confier à la vue la tâche de surveiller le modèle constamment afin de se modifier dès le moindre changement ?
En d'autres termes, le scénario est le suivant : la vue détecte les évènements, et transmet l'intention de l'utilisateur au contrôleur, qui ordonne la mise à jour au modèle, puis à la vue si besoin est. Bon cela dit, c'est bien beau de parler en termes théoriques, je vais vous montrer l'implémentation à laquelle j'ai réfléchi avec mon collègue Clément Laballe sur le sujet.
III. Implémentation du pattern MVC en J2ME▲
III-A. Architecture globale du système▲
On ne vous le répètera jamais assez : séparez en packages votre application. Elle y gagnera en lisibilité, et donc en facilité de maintenance. Rien que le fait de vous intéresser au pattern MVC prouve que vous voulez construire une appli souple, donc facilement évolutive, et je vous assure que l'ajout de fonctionnalités sera un réel plaisir (enfin, en tout cas, ce sera largement moins difficile que si votre code est mélangé pêle-mêle dans des classes fourre-tout…).
Je vous recommande donc après ce bref sermon l'architecture suivante :
Pour l'instant, chaque paquetage ne contient qu'un seul fichier java, mais pensez évolutivité : si vous désirez modifier le comportement d'une application, vous n'aurez qu'à créer un autre contrôleur… De même pour la vue, si vous désirez rajouter une autre vue, vous n'avez qu'à en créer une nouvelle. Ainsi, on peut tout à fait imaginer qu'en appuyant sur un bouton, on change dynamiquement de vue. Mais laissons cela pour plus tard, cela constituera un excellent exercice pour la fin de ce tutoriel.
III-B. La Midlet▲
Pour les néophytes, la classe Midlet revient à définir le comportement d'une application selon les états qui pourraient schématiser l'automate de vie d'une application embarqué sur téléphone mobile. Les méthodes qui y apparaissent déterminent donc le comportement de l'application face à un évènement. De base, 3 évènements sont nécessaires : la mise en route de l'application, sa mise en pause (mettons que vous receviez un coup de fil) ou sa destruction (si l'utilisateur souhaite quitter l'application par exemple). On pourrait résumer le cycle de vie d'une midlet au diagramme d'état-transitions suivant :
Concrètement, voici le code pour notre exemple :
/*
* MonAppli.java
*
* Created on 27 décembre 2007, 12:16
*/
package
midlet;
import
controller.MyController;
import
javax.microedition.midlet.*;
/**
*
*
@author
Raphael
*
@version
*/
public
class
MonAppli extends
MIDlet {
/* Il est préférable que le midlet puisse communiquer avec le controller */
private
MyController controller;
/**
* première méthode appelée lors du lancement de l'application mobile.
* C'est donc ici que l'on va instancier notre controler
**/
public
void
startApp
(
) {
this
.controller =
new
MyController
(
this
);
}
public
void
pauseApp
(
) {
}
public
void
destroyApp
(
boolean
unconditional) {
/* on signale au manager la destruction de l'appli */
this
.notifyDestroyed
(
);
}
}
III-C. Le contrôleur▲
C'est lui qui va se charger de toute la logique interactive entre l'utilisateur et l'application. Il doit pouvoir communiquer des ordres au modèle, mais aussi à la vue. Il se charge donc de les instancier, mais aussi de les détruire lorsque l'utilisateur manifeste son intention de quitter l'application.
/*
* MyController.java
*
* Created on 27 décembre 2007, 12:17
*
*/
package
controller;
import
javax.microedition.lcdui.Display;
import
javax.microedition.lcdui.Displayable;
import
midlet.MonAppli;
import
model.MyModel;
import
view.*;
/**
*
*
@author
WAESELYNCK Raphael
*/
public
class
MyController {
/* le controller doit pouvoir envoyer des messages au midlet (par exemple pour terminer l'application) */
private
MonAppli midlet;
private
MyView view;
private
MyModel model;
/** Creates a new instance of MyController */
public
MyController
(
MonAppli midlet) {
this
.midlet =
midlet;
this
.model =
new
MyModel
(
);
this
.view =
new
MyView1
(
this
, this
.model);
Display.getDisplay
(
this
.midlet).setCurrent
(
this
.getView
(
));
}
/**
* récupère l'intention de l'utilisateur lorsque celui-ci désire voir s'afficher la dernière touche appuyée
**/
public
void
userWantsToDisplayLastMove
(
int
move){
/* donc, dans l'ordre, on modifie le modèle..*/
this
.model.setMove
(
move);
this
.view.repaint
(
);
}
public
MyView getView
(
) {
return
this
.view;
}
/**
* l'utilisateur veut quitter l'application
**/
public
void
userWantsToExitAppli
(
) {
this
.view.stop
(
);
this
.model.stop
(
);
this
.view =
null
;
this
.model =
null
;
this
.midlet.destroyApp
(
false
);
}
}
III-D. La vue▲
La vue doit se charger d'afficher à l'utilisateur les informations qu'il souhaite connaître, et capter les désidératas de celui-ci pour les faire parvenir au contrôleur. Pour une appli J2ME avec le profil MIDP, cela se fait très facilement grâce à l'utilisation de la classe Canvas, qui permet à la fois de capter les interactions clavier ou tactile si le terminal mobile dispose d'un clavier, mais aussi de dessiner à l'écran de manière bas niveau. L'avantage est que le développeur peut créer une interface graphique en adéquation avec une certaine charte, ce que ne permet pas l'emploi de formulaires avec le profil MIDP par exemple.
Nous remarquons ici qu'il s'agit d'une classe abstraite. En effet, cela permet au controller de ne pas se préoccuper de l'implémentation de la vue, il lui laisse le soin de se dessiner toute seule comme une grande, donc il ne doit pas avoir à s'adapter à un cas particulier de vue. Si par exemple nous avons une vue qui affiche la dernière touche frappée en haut à gauche et une autre en bas à droite, le contrôleur ne s'en soucie pas, et ordonnera simplement à la vue d'afficher la dernière touche frappée. C'est à chaque vue d'implémenter la classe abstraite paint à sa guise en dessinant l'information à l'endroit souhaité. De la même manière, chaque vue pourra à loisir décider de changer son interaction avec l'utilisateur en changeant son interprétation quant à la signification des touches du clavier. Il suffira d'implémenter la méthode abstraite keyPressed.
/*
* MyView.java
*
* Created on 27 décembre 2007, 12:17
*
*/
package
view;
import
controller.MyController;
import
javax.microedition.lcdui.Canvas;
import
javax.microedition.lcdui.Graphics;
import
model.MyModel;
/**
*
*
@author
WAESELYNCK Raphael
*/
public
abstract
class
MyView extends
Canvas {
/**
* la vue doit pouvoir envoyer des messages au contrôleur pour lui signaler les intentions de l'utilisateur
* la vue doit pouvoir rapatrier des informations du modèle pour les afficher
**/
protected
MyController controller;
protected
MyModel model;
/** Creates a new instance of MyView */
public
MyView
(
MyController controller, MyModel model) {
this
.controller =
controller;
this
.model =
model;
}
/* cette méthode permet au Canvas de se dessiner */
public
abstract
void
paint
(
Graphics graphics);
/* cette méthode permet la détection d'évènements clavier */
public
abstract
void
keyPressed
(
int
keyCode);
/**
* termine proprement une vue
**/
public
void
stop
(
) {
//TODO : affecter à null tous les attributs de type objets lorsqu'il y en aura
}
}
A présent, voici un exemple d'implémentation de vue :
/*
* MyView1.java
*
* Created on 27 décembre 2007, 13:01
*
*/
package
view;
import
controller.MyController;
import
javax.microedition.lcdui.Graphics;
import
model.MyModel;
/**
*
*
@author
WAESELYNCK Raphael
*/
public
class
MyView1 extends
MyView {
/** Creates a new instance of MyView1 */
public
MyView1
(
MyController controller, MyModel model) {
super
(
controller, model);
}
/**
* Dessine sur l'élément graphics
**/
public
void
paint
(
Graphics graphics) {
int
height =
super
.getHeight
(
);
int
width =
super
.getWidth
(
);
graphics.setColor
(
0xFF0000
);
graphics.fillRect
(
0
,0
, height, width);
graphics.setColor
(
0x000000
);
/* au centre, on affiche la dernière touche appuyée par l'utilisateur */
graphics.drawString
(
""
+
super
.model.getMove
(
), height/
2
, width/
2
, Graphics.BASELINE|
Graphics.HCENTER);
}
/**
* récupération des évènements claviers
**/
public
void
keyPressed
(
int
keyCode) {
int
gameAction =
super
.getGameAction
(
keyCode);
/**
* on signale l'intention de l'utilisateur au contrôleur :
* - pour une touche en particulier (touche étoile), c'est que l'utilisateur veur quitter l'appli
* - pour toutes les autres, l'utilisateur veut voir leur code !
*/
if
(
keyCode ==
super
.KEY_STAR){
this
.controller.userWantsToExitAppli
(
);
}
else
{
super
.controller.userWantsToDisplayLastMove
(
gameAction);
}
}
}
III-E. Le modèle▲
Ici, très simple, le modèle se contente de mémoriser la dernière touche tapée par l'utilisateur sur le clavier. Il offre donc deux services : retenir le coup joué, et le rappeler, par le biais des méthodes (respectivement) setMove et getMove
/*
* MyModel.java
*
* Created on 27 décembre 2007, 12:17
*
*/
package
model;
/**
*
*
@author
WAESELYNCK Raphael
*/
public
class
MyModel {
/* pour l'exemple, notre modèle ne contiendra qu'une seule donnée: la touche tapée par l'utilisateur */
private
int
keyCode;
/** Creates a new instance of MyModel */
public
MyModel
(
) {
}
/**
* pour plus de propreté, on utilise les getters et les setters
**/
public
void
setMove (
int
keyCode){
this
.keyCode =
keyCode;
}
public
int
getMove (
){
return
this
.keyCode ;
}
}
IV. Modification de l'application▲
A présent, histoire de prouver une fois de plus que le MVC vaincra encore et toujours, nous allons mettre en œuvre ce dont nous parlions plus haut, à savoir l'ajout d'une nouvelle vue dans l'application. Histoire de rendre la chose un peu plus jolie, nous n'allons pas nous contenter de créer une nouvelle vue et de remplacer la ligne dans le contrôleur :
this
.view =
new
MyView1
(
this
, this
.model);
par :
this
.view =
new
MyView2
(
this
, this
.model);
Bien que cela puisse être aussi simple que cela ! Nous allons juste faire en sorte que dynamiquement, l'utilisateur puisse changer de vue en appuyant sur la touche FIRE par exemple. L'appui sur la touche STAR pour quitter l'application est conservé. Nous avons donc 2 choses à modifier :
- le contrôleur doit pouvoir capter une nouvelle intention de l'utilisateur qui est le souhait de changer la vue ;
- l'ancienne vue doit prendre en compte le nouveau cas particulier d'interaction clavier qu'est l'appui sur la touche FIRE
Voici donc le code à rajouter dans le contrôleur :
/**
* L'utilisateur souhaite changer de vue
**/
public
void
userWantsToChangeView
(
) {
this
.view.stop
(
);
//si l'on est sur une vue, on switche sur l'autre, et inversement...
if
(
this
.view instanceof
MyView1)
this
.view =
new
MyView2
(
this
, this
.model);
else
{
this
.view =
new
MyView1
(
this
, this
.model);
}
Display.getDisplay
(
this
.midlet).setCurrent
(
this
.view);
this
.view.repaint
(
);
}
et le code à modifier dans MyView1.java
if
(
keyCode ==
super
.KEY_STAR){
this
.controller.userWantsToExitAppli
(
);
}
else
if
(
gameAction==
super
.FIRE){
this
.controller.userWantsToChangeView
(
);
}
else
{
super
.controller.userWantsToDisplayLastMove
(
gameAction);
}
Et enfin, la nouvelle vue, MyView2 :
/*
* MyView2.java
*
* Created on 27 décembre 2007, 17:38
*
*/
package
view;
import
controller.MyController;
import
javax.microedition.lcdui.Graphics;
import
model.MyModel;
/**
*
*
@author
WAESELYNCK Raphael
*/
public
class
MyView2 extends
MyView{
/** Creates a new instance of MyView2 */
public
MyView2
(
MyController controller, MyModel model) {
super
(
controller, model);
}
public
void
paint
(
Graphics graphics) {
int
height =
super
.getHeight
(
);
int
width =
super
.getWidth
(
);
graphics.setColor
(
0x0000FF
);
graphics.fillRect
(
0
,0
, height, width);
graphics.setColor
(
0xFFFFFF
);
/* au centre, on affiche la dernière touche appuyée par l'utilisateur */
graphics.drawString
(
""
+
super
.model.getMove
(
), 0
, height, Graphics.BOTTOM|
Graphics.LEFT);
}
/**
* récupération des évènements claviers
**/
public
void
keyPressed
(
int
keyCode) {
int
gameAction =
super
.getGameAction
(
keyCode);
/**
* on signale l'intention de l'utilisateur au contrôleur :
* - pour une touche en particulier (touche étoile), c'est que l'utilisateur veut quitter l'appli
* - pour une autre, on change de vue (FIRE),
* - pour toutes les autres, l'utilisateur veut voir leur code !
*/
if
(
keyCode ==
super
.KEY_STAR){
this
.controller.userWantsToExitAppli
(
);
}
else
if
(
gameAction==
super
.FIRE){
this
.controller.userWantsToChangeView
(
);
}
else
{
super
.controller.userWantsToDisplayLastMove
(
gameAction);
}
}
}
On remarque au passage que rien n'a été modifié dans le modèle, que le code du contrôleur existant n'a pas eu à être modifié… Bien sûr des améliorations restent possibles, comme la factorisation du code concernant les interactions clavier qui pourrait être regroupé dans une fonction, à mettre dans une bibliothèque de fonctions d'interactions pourquoi pas… La gestion des fonctions et des librairies dans une application peut faire à elle seule l'objet d'un autre tutoriel !
V. Conclusion▲
On retiendra que le MVC est une solution robuste certes, mais plus couteuse en temps de développement. En revanche, le temps « perdu » lors de la conception d'une appli en MVC est regagnée lors des modifications telles que l'ajout de fonctionnalités comme vous l'avez constaté au fur et à mesure de ce tutoriel. Encore une fois, il ne s'agit pas ici d'imposer ma méthode, juste de proposer une solution pour pallier certains manques du CLDC dans Java ME. Merci d'avoir lu jusqu'ici, je suis disponible pour tout renseignement ou conseil sur JavaME par le biais du forum ou de message privés.