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é parBaptiste 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 du 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 ?

Image non disponible
schéma explicatif du pattern MVC

En d'autres termes, le scénario est le suivant : la vue détecte les évènements, et transmets 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 :

Image non disponible
architecture de package d'une application respectant je pattern MVC

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 :

Image non disponible
cycle de vie d'une midlet

Concrètement, voici le code pour notre exemple :

Code de la midlet
Sélectionnez

/*
 * 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.

code du contrôleur
Sélectionnez

/*
 * 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 simplement d'implémenter la méthode abstraite keyPressed.

Code de la vue
Sélectionnez

/*
 * 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 controleur 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 déssiner */
    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 :

implémentation d'une vue
Sélectionnez

/*
 * 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 controleur :
         *     - 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

code du modèle
Sélectionnez

/*
 * 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 :

 
Sélectionnez

this.view = new MyView1(this, this.model);

par :

 
Sélectionnez

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 :

 
Sélectionnez

/**
 * 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

 
Sélectionnez

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 :

code de la nouvelle vue
Sélectionnez

/*
 * 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 controleur :
         *     - pour une touche en particulier (touche étoile), c'est que l'utilisateur veur 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 possible, 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 palier à 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.