Gérer des pages dynamiques avec Android Studio

Notre objectif est de créer des pages dynamiques dans notre application Android Studio.

Dans l’article présentant la création de page web pour la gestion des interfaces utilisateurs Android, nous avons montré comment créer des pages statiques locales, enregistrées dans un répertoire local du terminal nommé Assets.

Si nous voulons créer des pages dynamiques avec Android Studio, gérées en local sur le terminal, nous avons besoin de mettre en place un serveur local avec une création à la volée des pages demandées. C’est ce que nous allons créer dans cet article. Nous allons construire un outil que nous appelerons ASP : Android Server Page. et nous utiliserons l’extensions ‘.asp‘ pour différentier ces pages des pages HTML statiques.

Nous pourrons ainsi : créer des pages dynamiques avec Android Studio pour des UI de qualité.

Android Server Page permettra d’utiliser des pages dynamiques avec Android Studio. Pour ce faire nous allons créer :

  • un gestionnaire générique pour la gestion de structure arborescence : Repository
  • un modèle de description de page : ScreenModel
  • un convertisseur pour générer le HTML à partir de notre modèle ScreenModel.

Créer des pages dynamiques pour Android Studio : une structure d’arbre.

En informatique, nous pouvons presque tout représenter dans une structure arborescente. La Classe Repository.java va permettre de gérer efficacement ces structures. Nous pourrons :

  • ajouter des méthodes en fonction de nos besoins
  • simplifier les parcours dans les arbres,
  • simplifier les recherches et les modifications des nœuds de l’arbre créé avec Repository.java.

Repository.java utilise deux classes complémentaires : RepositoryItem est utilisé pour chacun des nœuds de l’arbre, et TagItem pour aider à la construction des modèles.

La particularité de Repository.java est de porter la création de modèles définissant des Domain Specific Language(DSL).

La classe Repository.java

public class Repository <M extends Enum<M> & ModelWord> {
    private RepositoryKernel<M> kernel = new RepositoryKernel<M>();
    private String name;

    public Repository(String name) {
        this.name=name;
    }

    public RepositoryKernel getKernel() {
        return kernel;
    }

    /**
     * XmlTagItem porte l'ensemble des propriétés d'un Tag. Par exemple dans la lecture d'un XML.
     */
    public void addTag(XmlTagItem tag) {
        kernel.addTag(tag);
    }

    public String getRepositoryName() {
        return getKernel().getRoot().getName();
    }

    public RepositoryItem getRoot() {
        return getKernel().getRoot();
    }

    public RepositoryItem getCurrentNode() {
        return getKernel().getCurrentNode();
    }

    public RepositoryItem setCurrentNode(RepositoryItem node) {
        return getKernel().setCurrentNode(node);
    }

    public void broadcast(RepositoryBroadcast out) {
        out.startBroadcast();
        if (!getKernel().isEmpty()) {
            getRoot().broadcast(out, 0);
        }
        boolean doAgain = out.finishBroadcast();
        if (doAgain) {
            broadcast(out);
        }
    }

    public String getXmlString(){
        try {
            System.out.println("AppLog.Repository.getXmlString : début");
            XmlRepositoryByteWriter xmlRepositoryByteWriter = new XmlRepositoryByteWriter();
            broadcast(xmlRepositoryByteWriter);
            String xmlString = xmlRepositoryByteWriter.getString();
            System.out.println("AppLog.Repository.getXmlString : xmlString=" + xmlString);
            return xmlString;
        }catch(Exception e){
            System.out.println("AppLog.Repository.getXmlString : ERROR=" + e);
            e.printStackTrace();
        }
        return "ERROR";
    }
}

La classe Repository fonctionne avec une classe RepositoryKernel qui contient toute la structure d’arbre. Les traitements globaux sont regroupés dans la classe Repository.

Création de la classe RepositoryKernel.java :

public class RepositoryKernel<M extends Enum<M> & ModelWord>  {

    private RepositoryItem root = null;//le point d'entree du repository
    private RepositoryItem currentNode = null;//le noeud courrant

    public RepositoryKernel() {
    }

    protected boolean isEmpty() {
        return (root == null);
    }

    public RepositoryItem getRoot() {
        return root;
    }
    public void setRoot(RepositoryItem newRoot) {
        root = newRoot;
    }

    public RepositoryItem getCurrentNode() {
        return currentNode;
    }
    public RepositoryItem setCurrentNode(RepositoryItem newCurrentNode) {
        currentNode = newCurrentNode;
        return currentNode;
    }
    /**
     * Ajout du tag dans le repository en fonction de son type.
     */
    public void addTag(XmlTagItem<M> tag) {
        System.out.println("AppLog.RepositoryKernel.addTag : tag="+tag);
        switch (tag.getType()) {
            case END_TAG: {
                addEndTag(tag);
                break;
            }
            case SINGLE_TAG: {
                addSingleTag(tag);
                break;
            }
            case START_TAG: {
                addStartTag(tag);
                break;
            }
            case VALUE_TAG: {
                addValueTag(tag);
                break;
            }
            case UNDEFINE_TAG: {
                addUndefineTag(tag);
                break;
            }
        }
    }

    private void addStartTag(XmlTagItem<M> tag) {
        M myTagName = tag.getModel();
        String myName = tag.getPropertyName();
        if (myName == null) {
            myName = getCurrentNode().getNameId();
        }
        RepositoryItem myTagObject = new RepositoryItem(myTagName, myName, getCurrentNode());
        myTagObject.setProperties(tag.getProperties());
        if (isEmpty()) {
            setRoot(myTagObject);
            setCurrentNode(myTagObject);
        } else {
            getCurrentNode().addChild(myName, myTagObject);
            setCurrentNode(myTagObject);
        }
        System.out.println("AppLog.RepositoryKernel.addSingleTag : myTagObject="+myTagObject);
    }

    private void addSingleTag(XmlTagItem<M> tag) {
        //PlugsetLog.println("tag=" + tag);
        M myTagName = tag.getModel();
        String myName = tag.getPropertyName();
        if (myName == null) {
            myName = getCurrentNode().getNameId();
        }
        //on ajoute le tag au repository
        RepositoryItem myTagObject = new RepositoryItem(myTagName, myName, getCurrentNode());
        myTagObject.setProperties(tag.getProperties());
        String textValue = tag.getText();
        myTagObject.setValue(textValue);
        if (isEmpty()) {
            setRoot(myTagObject);
            setCurrentNode(myTagObject);
        } else {
            getCurrentNode().addChild(myName, myTagObject);
        }
        System.out.println("AppLog.RepositoryKernel.addSingleTag : myTagObject="+myTagObject);
    }

    private void addEndTag(XmlTagItem<M> tag) {
        ConcurrentHashMap<KeyWord, Object> properties = tag.getProperties();
        if (properties != null && !properties.isEmpty()) {
            for (KeyWord keyName : properties.keySet()) {
                Object keyValue = properties.get(keyName);
                getCurrentNode().setProperty( keyName, keyValue);
            }
        }
        System.out.println("AppLog.RepositoryKernel.addEndTag : getCurrentNode()="+getCurrentNode());
        if (getCurrentNode().hasParent()) {
            setCurrentNode(getCurrentNode().getParent());
        }
    }

    private void addValueTag(XmlTagItem<M> tag) {
        getCurrentNode().setValue(tag.getText());
    }

    private void addUndefineTag(XmlTagItem<M> tag) {
    }

}

XmlTagItem contient tous les éléments du tag. Les propriétés des tags sont enregistrées dans un ConcurrentHashMap (Key,Value). Nous créons un Enum KeyWord qui va regrouper toutes les clés dont nous aurons besoin.

Création de XmlTagItem.java

/**
 * Generique utilisé avec MODEL (ScreenWord)
 * @param <M>
 */
public class XmlTagItem <M extends Enum<M> & ModelWord> {

    private M model ;//le nom du tag
    private XmlTagItemType type = XmlTagItemType.UNDEFINE_TAG;
    private java.util.concurrent.ConcurrentHashMap<KeyWord, Object> properties = new ConcurrentHashMap<KeyWord, Object>();
    private java.lang.String textValue = "";

    public XmlTagItem() {
    }

    public boolean isType(XmlTagItemType myType) {
        return type == myType;
    }

    public void setType(XmlTagItemType myType) {
        this.type = myType;
    }

    public void setTagSingle() {
        setType(XmlTagItemType.SINGLE_TAG);
    }

    public void setTagEnd() {
        setType(XmlTagItemType.END_TAG);
    }

    public void setTagStart() {
        setType(XmlTagItemType.START_TAG);
    }

    public XmlTagItemType getType() {
        return type;
    }

    public void setModel(M model) {
        this.model = model;
    }

    public M getModel() {
        return model;
    }

    public void setText(String textValue) {
        this.textValue = textValue;
    }

    public String getText() {
        return textValue;
    }

    public ConcurrentHashMap<KeyWord, Object> getProperties() {
        return properties;
    }

    public void addProperty(KeyWord att, Object val) {
        properties.put(att, val);
    }

    public Object getProperty(KeyWord name) {
        if (properties.containsKey(name)) {
            return properties.get(name);
        } else {
            return null;
        }
    }

    /**
     * donne la valeur associée à la proprité de nom 'name'
     */
    public String getPropertyName() {
        if (properties.containsKey(KeyWord.NAME)) {
            return properties.get(KeyWord.NAME).toString();
        }
        return null;
    }

    public String toString(){
        StringBuilder result=new StringBuilder();
        result.append(getModel().toString());
        result.append(" ");
        result.append(getType().toString());
        result.append(" ");
        result.append(getProperties().toString());
        return result.toString();
    }
}

Nous avons créé une Enum pour gérer les différents types de XmlTagItem.

Création de XmlTagItemType.java :

public enum XmlTagItemType {
    END_TAG,
    START_TAG,
    VALUE_TAG,
    SINGLE_TAG,
  	UNDEFINE_TAG;
}

Nous voyons que nous utilisons une liste de mot AttWord pour définir les noms des attributs de notre tag. Nous créerons à chaque fois que nous en aurons besoin un nouveau nom d’attribut.

La classe Enum KeyWord.java :

public enum KeyWord {

    ACTION_NAME("actionName"),
    ACTION_PARAM("actionParam"),
    CAPTION("caption"),
    EDITABLE("editable"),
    NAME("name"),
    AREA_START("areaStart"),
    AREA_END("areaEnd");

    private String value;

    private static final Map<String, KeyWord> BY_LABEL = new HashMap();

    static{
        for(KeyWord word:values()){
            BY_LABEL.put(word.value,word);
        }
    }

    private KeyWord(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return getValue();
    }

    public static KeyWord parse(String value)  {
        return BY_LABEL.get(value);
    }
}

Définir les noeuds de notre structure arborescente

RepositoryItem contient un nœud de l’arbre.

Classe RepositoryItem.java

public class RepositoryItem <M extends Enum<M> & ModelWord> {

    private M model;//le model défini le type de tag
    private String name;//nom unique pour le RepositoryItem (une propriété obligatoire)
    private RepositoryItem parent;//contient le pere du RepositoryItem
    private ConcurrentHashMap<KeyWord, Object> properties = new ConcurrentHashMap<>(); //contient toutes les propriétés definis pour le RepositoryItem
    private LinkedHashMap<String, RepositoryItem> children = new LinkedHashMap<String, RepositoryItem>();
    private int itemId = 0;//numero utilise pour créer des references au nom unique.
    private String value;
    private static final String LABEL="boxet";

    public RepositoryItem(M model, String name, RepositoryItem parent) {
        this.model = model;
        this.name = name;
        this.parent = parent;
    }

    public ConcurrentHashMap<KeyWord, Object> getProperties() {
        return properties;
    }
    public void setProperties(ConcurrentHashMap<KeyWord, Object> properties) {
        this.properties = properties;
    }
    public void setProperty(KeyWord att, Object val) {
        if (properties == null) {
            properties = new ConcurrentHashMap<KeyWord, Object>();
        }
        if (att != null && val != null) {
            properties.put(att, val);
        }
    }
    public Object getProperty(KeyWord att){
        return properties.get(att);
    }
    public ModelWord getModel() {
        return (ModelWord)model;
    }

    public String getName() {
        return name;
    }
    private int getItemId() {
        itemId = itemId + 1;
        return itemId;
    }
    public String getNameId() {
        return LABEL + getItemId();
    }
    public void setValue(String value) {
        this.value = value;
    }

    public void addChild(String name, RepositoryItem tagObject) {
        children.put(name, tagObject);
    }
    public boolean hasParent() {
        if (getParent() == null) {
            return false;
        } else {
            return true;
        }
    }
    public RepositoryItem getParent() {
        return parent;
    }
    /**
     * Méthode de diffusion des informations du TagObject vers une sortie
     * d'edition OutLine. la méthode est récursive et appel chacun des TagObject.
     */
    public void broadcast(RepositoryBroadcast out, int level) {
        out.startVisit(this, level);
        int nextLevel = level + 1;
        broadcastChildren(out, nextLevel);
        boolean doAgain = out.finishVisit(this, level);
        if (doAgain) {
            broadcast(out, level);
        }
    }

    private void broadcastChildren(RepositoryBroadcast out, int level) {
        for (RepositoryItem child : children.values()) {
            child.broadcast(out, level);
        }
    }

    public Set<KeyWord> listPropertyNames() {
        return properties.keySet();
    }

    public boolean hasChildren() {
        return !children.isEmpty();
    }

    public boolean hasValue() {
        return (value != null && value.length() != 0);
    }

    public String getValue() {
        return value;
    }

    public String toString() {
        StringBuilder myReturn = new StringBuilder();
        myReturn.append(model.getValue()) ;
        myReturn.append(" " );
        myReturn.append(properties.toString());
        return myReturn.toString();
    }
}

Pour utiliser nos outils, nous créons une interface de diffusion qui permet de projeter notre structure d’arbre afin de réaliser des traitements sur ces arbres.

L’interface RepositoryBroadcast.java

public interface RepositoryBroadcast {

    /**
     * Méthode invoquée pour le repository au debut du broadcast..
     */
    boolean startBroadcast();

    /**
     * Méthode invoquée au début de la visite du RepositoryItem par le broadcast...
     */
    boolean startVisit(RepositoryItem tagObject, int level);
  
    /**
     * Méthode invoquée à la fin de la visite de l'élément RepositoryItem par le broadcast...
     */
    boolean finishVisit(RepositoryItem tagObject, int level);
  
    /**
     * Méthode invoquée pour le repository à la fin du broadcast..
     */
    boolean finishBroadcast();
}

En mettant en œuvre cet interface, nous pouvons exploiter toute la puissance du Repository. C’est ce que nous allons voir pour la création dynamique d’interfaces UI Android de qualité dans le paragraphe suivant.

Nous avons ici la structure minimaliste pour gérer de façon extensible une arborescence, et donc nos modèles. Cette structure sera étendue au fur et à mesure de nos besoins. En attendant, nous avons ici de quoi créer les interface UI dynamique pour des application Android de qualité.

Créer les pages dynamiques pour Android Studio, les modèles.

Nous allons définir toutes les pages de notre application à partir d’un modèle de description de pages. Nous verrons au fur et à mesure comment compléter notre modèle avec de nouveaux tag selon nos besoins.

Dans cette application nous visons exclusivement les terminaux de type smartphone. Aussi, nous ne gérerons pas le progressive design qui permet de changer la disposition des éléments dans la page pour des surfaces d’écran plus grands.

Nous créons un modèle générique pour tous nos modèle créés avec Repository. Ce modèle générique sera spécialisé pour chaque modèle. Ici nous créerons le modèle ScreenPage pour les Pages qui utilisera la bibliothèque ScreenLibrary. Mais entrons dans le vif du sujet.

La classe Model.java

public abstract class Model {
  private Repository repository;
  private PlugsetMessage plugsetMessage;
  private SquareMap <String, Integer, Object> modelMemory= new SquareMap <String, Integer, Object> ();
  
  public Model(){
  }
  
  public Repository getRepository(){
    return repository;
  }
  public void setRepository(Repository repository){
    this.repository=repository;
  }
  
  public PlugsetMessage getPlugsetMessage(){
    return plugsetMessage;
  }
  public void setPlugsetMessage(PlugsetMessage plugsetMessage){
    this.plugsetMessage=plugsetMessage;
  }
    
  public SquareMap <String, Integer, Object> getModelMemory(){
    return modelMemory;
  }
  public void setModelMemory(SquareMap <String, Integer, Object> modelMemory){
    this.modelMemory=modelMemory;
  }
  
  public abstract PlugsetMessage build(PlugsetMessage message);
}

Cette classe toute simple défini un Modèle générique.

Pour créer la partie dynamique de nos écrans, nous avons besoin d’adapter la construction de la page en fonction du contexte. Le contexte nous est donné par PlugsetMessage, qui regroupe toutes les informations de la session.

La gestion des Messages et des données temporaires lors de la création des pages dynamiques

L’application gagne à être architecturée en envoi de messages asynchrone. Nous avons besoin de messages pour communiquer entre les pages et la partie java Android, mais aussi entre le terminal et le serveur. Nous pouvons aussi utiliser des messages à l’intérieure d’une même page lors de la réception d’évènements interne à la page ou externe à la page en cours.

Création de PlugsetMessage.java

public class PlugsetMessage {

    public SquareMap<String, String, Object> data = new SquareMap<String, String, Object>();

    public PlugsetMessage() {
    }

    public void put(String field, String name, Object value) {
        data.put(field, name, value);
    }
    public Object get(String field, String name) {
        return data.get(field, name);
    }
}

Nous avons créé une classe générique SquareMap comme zone mémoire permettant de conserver des calculs intermédiaires.

Nous utiliserons PlugsetMessage pour gérer les flux de données entre la partie interface utilisateur (UI) et la partie java de l’application Android. Cela permettra de créer des interfaces utilisateur (UI) de qualité.

La création de la librairie ScreenLibrary

Pour construire le modèle, nous utiliserons la méthode build(PlugsetMessage).

Chaque Langage Spécifique à un Domaine va disposer de méthodes décrivant le langage. Nous allons montrer comment nous construisons notre langage de construction d’écran dynamique.

Création de ScreenLibrary.java

public class ScreenLibrary {
    private Model model;

    public ScreenLibrary(Model model) {
        this.model=model;
    }

    /**
     * methode get() pour accéder au repository du Modèle, ajoutée pour simpliifer l'écriture des tags.
     */
    public Repository get(){
        return model.getRepository();
    }

    /**
     * initialisation de la page et du reepository qui contiendra la page.
     */
    public void newPage(String name, String areaStart, String areaEnd) {
        model.setRepository(new Repository(name));
        model.setModelMemory(new SquareMap<String, Integer, Object>());
        XmlTagItem<ScreenWord> tag = new XmlTagItem<ScreenWord>();
        tag.setTagStart();
        tag.setModel(ScreenWord.SCREEN);
        tag.addProperty(KeyWord.NAME, name);
        tag.addProperty(KeyWord.AREA_START,areaStart);
        tag.addProperty(KeyWord.AREA_END, areaEnd);
        get().addTag(tag);
    }
    public void endPage() {
        XmlTagItem<ScreenWord> tag = new XmlTagItem<ScreenWord>();
        tag.setTagEnd();
        tag.setModel(ScreenWord.SCREEN);
        get().addTag(tag);
    }

    public final void label(String name, String caption, String areaStart, String areaEnd) {
        XmlTagItem<ScreenWord> tag = new XmlTagItem<ScreenWord>();
        tag.setTagSingle();
        tag.setModel(ScreenWord.LABEL);
        tag.addProperty(KeyWord.NAME, name);
        tag.addProperty(KeyWord.CAPTION, caption);
        tag.addProperty(KeyWord.AREA_START,areaStart);
        tag.addProperty(KeyWord.AREA_END, areaEnd);
        get().addTag(tag);
    }

    public void button(String name, String caption, String areaStart, String areaEnd, String actionScriptName, String actionParam, boolean editable) {
        XmlTagItem<ScreenWord> tag = new XmlTagItem<ScreenWord>();
        tag.setTagSingle();
        tag.setModel(ScreenWord.BUTTON);
        tag.addProperty(KeyWord.NAME, name);
        tag.addProperty(KeyWord.CAPTION, caption);
        tag.addProperty(KeyWord.AREA_START,areaStart);
        tag.addProperty(KeyWord.AREA_END, areaEnd);
        tag.addProperty(KeyWord.ACTION_NAME, actionScriptName);
        tag.addProperty(KeyWord.ACTION_PARAM, actionParam);
        tag.addProperty(KeyWord.EDITABLE, editable);
        get().addTag(tag);
    }
}

En ligne 18, nous définissons newPage(). Les propriétés sizeStart et sizeStop sont utilisées pour définir l’organisation de la page. Nous dessinons chaque page selon une grille avec comme pour Excel, les nom de colonne avec des lettres et les nom des lignes avec des chiffres. La première cellule en haut à gauche est ‘A1’. La dernière cellule en bas à droite permet de calculer le nombre de colonnes et le nombre de lignes. Le positionnement de chaque composant sur la page se fera avec un espace défini en sizeStart et en sizeStop.

même, pour le modèle ScreenLibrary, nous avons des ScreenWord définissant le langage du domaine utilisés comme modèle dans les tags.

La classe Enum ScreenWord.java

public enum ScreenWord implements ModelWord {

    BUTTON("Button"),
    LABEL("Label"),
    SCREEN("Screen");

    private final String value;

    private ScreenWord(String value) {
        this.value = value;
    }

    @Override
    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return value;
    }

    public static ScreenWord parse(String value){
        return  ModelWord.parse(ScreenWord.class,  value);
    }
}

ModelWord.java est un interface imposant getValue(), et définissant la méthode parse(String name).

L’interface ModelWord utilise la gestion des enum avec les generic java.

public interface ModelWord {

    String getValue();
    
    public static <M extends Enum<M> & ModelWord> M parse(Class<M> enumClass,  String myValue){
        for(M constant : enumClass.getEnumConstants()){
            if(myValue.contains(constant.getValue())){
                return constant;
            }
        }
        return null;
    }
}

Un modèle pour créer des pages dynamiques Android Studio

Nous avons vu dans la description des pages de notre application les composants dont nous avons besoin pour dessiner l’interface UI.

Nous allons définir notre classe ScreenModel qui sera utilisée pour dessiner les interfaces.

La page type de notre application

Pour notre application, nous définissons une page type abstraite qui sera déclinée pour chacune des pages.

Création de la page abstraite ScreenModel :

public abstract class ScreenModel extends Model {
  
  Context context;
  
   public ScreenModel(Context context) {
       	super();
     	this.context=context;
     	
   }
  
  	@javascriptInterface
   	public PlugsetMessage get(PlugsetMessage message) {
        return build(message);
   	}
  
  	@javascriptInterface
   	public PlugsetMessage post(PlugsetMessage message) {
        return build(message);
   	}
  
}

C’est la page modèle de l’application. Nous pourrons compléter cette classe par la définition de style à utiliser dans toute l’application.

La page Menu de l’application

La page menu est constituée de boutons permettant d’aller sur chacune des pages de l’application. C’est une page très simple que nous créons comme suit :

public class Menu extends ScreenModel {

    Context context;

    public Menu(Context context) {
        super(context);
    }

    public PlugsetMessage build(PlugsetMessage message) {
        ScreenLibrary s = new ScreenLibrary(this);
        s.newPage("menu", "A1", "A12");
        s.label("titre", "menu", "A1", "A1");
        String userName=getUser(message);
        s.button("monCompte", "mon compte : " + userName, "A3", "A3", "callScreen", "mon-compte.asp", true);
        s.button("rangement", "mes lieux de rangement", "A4", "A4", "callScreen", "mes-lieux-de-rangement.asp", true);
        s.button("rechercher", "rechercher un livre", "A5", "A5", "callScreen", "rechercher-un-livre.asp", true);
        s.button("identifier", "identifier un livre", "A6", "A6", "callScreen", "identifier-un-livre.asp", true);
        s.button("ranger", "ranger un livre", "A7", "A7", "callScreen", "ranger-un-livre.asp", true);
        s.button("inventaire", "inventaire sur un lieux", "A8", "A8", "callScreen", "inventaire-sur-un-lieux.asp", true);
        s.button("mesAmis", "mes amis", "A9", "A9", "callScreen", "mes-amis.asp", true);
        s.button("preter", "prêter un livre", "A10", "A10", "callScreen", "preter-un-livre.asp", true);
        s.button("pretEnCours", "prêt en cours", "A11", "A11", "callScreen", "pret-en-cours.asp", true);
        s.endPage();
        Repository screenRepository=s.get();
        System.out.println("AppLog.Menu.build : screenRepository="+screenRepository.getXmlString());
        //on doit construire la réponse et la mettre dans message
        message.put("page","xml",getRepository());
        return message;
    }

    private String getUser(PlugsetMessage message){
        return "veuillez-vous identifier";
    }

}

Nous notons que cette page suppose de connaitre le nom de l’utilisateur connecté. Pour l’instant, nous mettons un libellé de façon statique. Nous verrons ultérieurement comment gérer la partie dynamique en prenant le nom dans :

  • le message PlugsetMessage passé en paramètre ou
  • dans modelMemory défini comme une SquareMap au niveau du modèle de la page,
  • ou encore dans Context pour un accès global au context de l’application Android.

La page Menu construit notre repository que nous pouvons éditer en xml. Les lignes 24 et 25 permettent d’afficher le xml dans la trace LogCat d’Android Studio :

<Screen name="menu" sizeStart="A1" sizeStop="A12">	
<Label name="titre" caption="menu" sizeStart="A1" sizeStop="A1"/>	
<Button name="monCompte" actionName="callScreen" caption="mon compte : veuillez-vous identifier" sizeStart="A3" sizeStop="A3" editable="true" actionParam="mon-compte.asp"/>	
<Button name="rangement" actionName="callScreen" caption="mes lieux de rangement" sizeStart="A4" sizeStop="A4" editable="true" actionParam="mes-lieux-de-rangement.asp"/>	
<Button name="rechercher" actionName="callScreen" caption="rechercher un livre" sizeStart="A5" sizeStop="A5" editable="true" actionParam="rechercher-un-livre.asp"/>	
<Button name="identifier" actionName="callScreen" caption="identifier un livre" sizeStart="A6" sizeStop="A6" editable="true" actionParam="identifier-un-livre.asp"/>	
<Button name="ranger" actionName="callScreen" caption="ranger un livre" sizeStart="A7" sizeStop="A7" editable="true" actionParam="ranger-un-livre.asp"/>	
<Button name="inventaire" actionName="callScreen" caption="inventaire sur un lieux" sizeStart="A8" sizeStop="A8" editable="true" actionParam="inventaire-sur-un-lieux.asp"/>	
<Button name="mesAmis" actionName="callScreen" caption="mes amis" sizeStart="A9" sizeStop="A9" editable="true" actionParam="mes-amis.asp"/>	
<Button name="preter" actionName="callScreen" caption="prêter un livre" sizeStart="A10" sizeStop="A10" editable="true" actionParam="preter-un-livre.asp"/>	
<Button name="pretEnCours" actionName="callScreen" caption="prêt en cours" sizeStart="A11" sizeStop="A11" editable="true" actionParam="pret-en-cours.asp"/>
</Screen>

Ce xml peut ensuite est converti en html.

Commençons par générer le html de la page Menu, puis nous compléterons notre outil avec plus de fonctionnalités.

Créer les pages HTML dynamiquement à partir de notre modèle pour Android Studio

La description des pages avec ScreenLibrary doit maintenant être converti en HTML. Cette conversion se fait grâce à un parcours d’arbre récursif défini dans Repository.java. Nous projetons notre modèle sur un convertisseur HTML. Chaque nœud du modèle correspond à un traitement venant ajouter des éléments de CSS/javascript/html afin de constituer la page finale.

La génération utilise :

  • RepositoryEngine
  • HtmlEngine

L’introspection au cœur du langage java

Au cœur de la génération html, nous utilisons l’introspection. L’introspection est la propriété d’un programme java à accéder à des classes java compilées et présentes dans le programme java, c’est à dire dans le jar en train de s’exécuter.

A chaque nom de tag ScreenWord, nous aurons une méthode du même nom dans HtmlEngine. Cette méthode sera appelée grâce à la projection Broadcast dont nous avons vu l’implémentation dans les classes Repository et RepositoryItem.

Exemple, pour le ScreenWord ‘button‘, nous avons dans HtmlEngine, les méthodes :

  • button_start(RepositoryItem tag)
  • button_finish(RepositoryItem tag)

Dans la page Menu la méthode build() construit le Repository. Nous parcourrons ce Repository et à chaque mot ScreenWord rencontré, nous appelons dans l’ordre de parcours les méthodes « _start » et « _finish » correspondantes.

La classe RepositoryEngine.java :

public abstract class RepositoryEngine<M extends Enum<M> & ModelWord> implements RepositoryBroadcast {

    private Set unknownStartMethod = new TreeSet();
    private Set unknownFinishMethod = new TreeSet();
    public boolean executeWithError = false;
    public String errorMsg = null;

    public abstract String getMethodName(ModelWord word, String suffix);

    public boolean doVisit(RepositoryItem tagObject, int level, String suffix) {
        //System.out.println(this.getClass().getName()+ ".finishVisit tagObject=" + tagObject.toString());
        String methodName = getMethodName(tagObject.getModel(), suffix);
        if (methodName == null) {
            return false;
        }
        if (unknownFinishMethod.contains(methodName)) {
            return false;
        }
        //System.out.println(this.getClass().getName()+ ".finishVisit methodName=" + methodName);
        Class returnType = null;
        try {
            Class[] paramClasses = {RepositoryItem.class};
            Method mthd = this.getClass().getMethod(methodName, paramClasses);
            returnType = mthd.getReturnType();
            //System.out.println(this.getClass().getName()+ " returnType=" + returnType + " destUse=" + destUse + " methodName=" + methodName);
            Object params[] = {tagObject};
            Boolean doAgain = (Boolean) mthd.invoke(this, params);
            return doAgain;
        } catch (InvocationTargetException e) {
            System.out.println(this.getClass().getName()+ ".finishVisit " + tagObject.toString() + " InvocationTargetException "+ e);
        } catch (NoSuchMethodException e) {
            unknownFinishMethod.add(methodName);
            //System.out.println(this.getClass().getName()+  ".finishVisit " + tagObject.toString() + " NoSuchMethodException ", e);
        } catch (IllegalAccessException e) {
            System.out.println(this.getClass().getName()+ ".finishVisit " + tagObject.toString() + " IllegalAccessException "+ e);
        } catch (Exception e) {
            //le fait d'avoir une valeur de retour est un chngement qui n'est plus utilisé
            //System.out.println(this.getClass().getName()+  ".finishVisit " + tagObject.toString() + " Exception "+ e);
        }
        return false;
    }

    /**
     * startBroadcast permet par exemple de créer  des ouvertures d'accès à des fichiers.
     */
    @Override
    public abstract boolean startBroadcast() ;

    /**
     * startVisit va récupérer le model du RepositoryItem et demander le nom de la méthoe associée.
     * puis avec introspection executer la méthode trouvée en passant RepositoryItem en paramètre.
     */
    @Override
    public boolean startVisit(RepositoryItem tagObject, int level) {
        return doVisit(tagObject,  level, "_start");
    }

    /**
     * FinishVisit cloture le tag. Certains tag n'ont pas de tag de fin. nous
     * devons donc vérifier si un tag de fin est necessaire.
     */
    @Override
    public boolean finishVisit(RepositoryItem tagObject, int level) {
        return doVisit(tagObject,  level,"_finish");
    }

    /**
     * finishBroadcast permer de fermer des accès aux fichiers.
     */
    public abstract boolean finishBroadcast() ;

    private void printMethod(Method[] methods) {
        Method m;
        System.out.println("Liste des methodes");
        for (int x = 0; x < methods.length; x++) {
            m = methods[x];
            System.out.println(m.getName());
        }
    }
}

RepositoryEngine permet de traiter les tag du Repository par la méthode broadcast :

Rappel de la méthode broadcast de Repository :

    /**
    * methode de Repository :
    * broadcast projete le Repository sur le RepositoryBroadcast
    */
    public void broadcast(RepositoryBroadcast out) {
        out.startBroadcast();
        if (!getKernel().isEmpty()) {
          getRoot().broadcast(out, 0);
        }
        boolean doAgain = out.finishBroadcast();
        if (doAgain) {
          broadcast(out);
        }
    }

Comme RepositoryEngine implement RepositoryBroadcast, les visites des noeud du Repository seront appelées et permettrons la conversion du Repository.

C’est grâce à RepositoryBroadcast que nous parvenons à convertir le descriptif de la page pour créer des pages dynamiques pour les applications Android Studio, avec des Interfaces Utilisateurs (UI) de qualité.

Création du convertisseur de la page en html

La conversion de la page en html suppose de créer une classe HtmlEngine qui étend RepositoryEngine et implémente les méthodes abstraites.

On notera l’utilisation des génériques dans RepositoryEngine qui permet de fournir à RepositoryEngine le ModelWord compatible avec le ModelEngine implémenté.

Création de HtmlRepositoryEngine afin de fixer le ModelWord utilisé :

public abstract class HtmlRepositoryEngine extends RepositoryEngine<ScreenWord> {

    private EnumMap<ScreenWord, String> tagMethodMatch= new EnumMap<ScreenWord, String>(ScreenWord.class);

    protected StringBuilder css=new StringBuilder();
    protected StringBuilder htmlBody=new StringBuilder();
    protected StringBuilder javascript=new StringBuilder();
    protected StringBuilder html=new StringBuilder();
    protected String title="";

    public HtmlRepositoryEngine() {
        super();
        loadTagMethodMatch();
    }
    
    private void addWordMethodMatch(ScreenWord word, String methodName){
        tagMethodMatch.put(word,methodName);
    }

    private void loadTagMethodMatch() {
        addWordMethodMatch( ScreenWord.SCREEN,  "screen");
        addWordMethodMatch( ScreenWord.LABEL,  "label");
        addWordMethodMatch( ScreenWord.BUTTON,  "button");
    }

    /**
     * Le suffix est soit '_start' soit '_finish'
     */
    public String getMethodName(ModelWord modelWord, String suffix) {
        try {
            ScreenWord word=(ScreenWord)modelWord;
            if (tagMethodMatch.containsKey(word)) {
                return tagMethodMatch.get(word) + suffix;
            } else {
                return word.getValue() + suffix;
            }
        } catch (Exception e) {
            System.out.println("ERROR : tag=" + modelWord + " should be declared in " + this.getClass().getName() + " Exception:"+ e);
        }
        return modelWord.getValue() + suffix;
    }

    @Override
    public boolean startBroadcast() {
        css=new StringBuilder();
        htmlBody=new StringBuilder();
        javascript=new StringBuilder();
        html=new StringBuilder();
        title="";
        return false;
    }

    @Override
    public boolean finishBroadcast() {

        html.append("<!DOCTYPE html>");
        html.append("<html>");

        html.append("<head>");

        html.append("<title>");
        html.append(title);
        html.append("</title>");

        html.append(getHtmlStyle());
        html.append(getHtmlScript());

        html.append("</head>");
        html.append(getHtmlBody());
        html.append("</html>");
        return false;
    }

    protected abstract String getHtmlBody();
    protected abstract String getHtmlStyle();
    protected abstract String getHtmlScript();

    public String getHtml(){
        return html.toString();
    }
}

En ligne 20 et suivante, nous indiquons le nom des méthodes que nous devons implémenter pour traiter chacun des Tag.

Presentation de la page à générer

Nous allons présenter la page à générer à partir du model de la page Menu

<!DOCTYPE html>
<html>
<head>
    <title>Menu</title>
    <style>
      body {
        display: grid;
        grid-template-columns:1fr;
        grid-template-rows:repeat(12,1fr);
      }
      .item_A1_A1 {
        grid-column:1/2;
        grid-row:1/2;
      }
      .item_A3_A3 {
        grid-column:1/2;
        grid-row:3/4;
      }
      .item_A4_A4 {
        grid-column:1/2;
        grid-row:4/5;
      }
      .item_A5_A5 {
        grid-column:1/2;
        grid-row:4/5;
      }
      .item_A6_A6 {
        grid-column:1/2;
        grid-row:6/7;
      }
      .item_A7_A7 {
        grid-column:1/2;
        grid-row:7/8;
      }     
      .item_A8_A8 {
        grid-column:1/2;
        grid-row:8/9;
      }      
      .item_A9_A9 {
        grid-column:1/2;
        grid-row:9/10;
      }      
      .item_A10_A10 {
        grid-column:1/2;
        grid-row:10/11;
      }
      .item_A11_A11 {
        grid-column:1/2;
        grid-row:11/12;
      }
  </style>
</head>
<body>
    <div class="item_A1_A1">Menu</div>
    <button type="button" class="item_A3_A3" onclick="call-screen('mon-compte.asp')">
        mon compte : veuillez vous identifier
    </button>
    <button type="button" class="item_A4_A4" onclick="call-screen('mes-lieux-de-rangement.asp')">
        mes lieux de rangement
    </button>
    <button type="button" class="item_A5_A5" onclick="call-screen('rechercher-un-livre.asp')">
        rechercher un livre
    </button>
    <button type="button" class="item_A6_A6" onclick="call-screen('identifier-un-livre.asp')">
        identifier un livre
    </button>
    <button type="button" class="item_A7_A7" onclick="call-screen('ranger-un-livre.asp')">
        ranger un livre
    </button>
    <button type="button" class="item_A8_A8" onclick="call-screen('inventaire-sur-un-lieux.asp')">
        inventaire sur un lieux
    </button>
    <button type="button" class="item_A9_A9" onclick="call-screen('mes-amis.asp')">
        mes amis
    </button>
    <button type="button" class="item_A10_A10" onclick="call-screen('preter-un-livre.asp')">
        prêter un livre
    </button>
    <button type="button" class="item_A11_A11" onclick="call-screen('pret-en-cours.asp')">
        prêt en cours
    </button>
</body>
</html>

La création des pages html se fait à la volée avec les descriptions données dans chaque page ScreenModel, ici la Page Menu.

Gestion du positionnement des composants dans la page avec une grille

Nous définissons une grille pour placer chaque élément sur la page. Chaque élément est représenté par une cellule de départ AreaStart et une cellule de fin AreaEnd. Nous avons choisi la notation au format des tableaurs : colonne en lettre et ligne en chiffre, avec A1 pour la cellule en haut à gauche.

Nous créons une classe AreaCell qui permet de gérer l’emplacement d’un composant dans la grille :

public class AreaCell {
    
    public String areaStart;
    public String areaEnd;
    public int column_start;
    public int column_end;
    public int row_start;
    public int row_end;

    public AreaCell(String areaStart, String areaEnd){
        this.areaStart =areaStart;
        this.areaEnd =areaEnd;
        setAreaStart(areaStart);
        setAreaEnd(areaEnd);
    }

    private String getColumnName(String cellName){
        StringBuilder columnName=new StringBuilder();
        for(int i=0;i<cellName.length();i++) {
            char c = cellName.charAt(i);
            if(!Character.isDigit(c)){
                columnName.append(c);
            }
        }
        return columnName.toString();
    }

    private String getLineName(String cellName){
        StringBuilder lineName=new StringBuilder();
        for(int i=0;i<cellName.length();i++) {
            char c = cellName.charAt(i);
            if(Character.isDigit(c)){
                lineName.append(c);
            }
        }
        return lineName.toString();
    }
    
    private int columnIndex(String columnName){
        int index=0;
        for(int i=0;i<columnName.length();i++) {
            char c = columnName.charAt(i);
            int ajout=(c-'A');
            int power=(int)Math.pow(26,i);
            index=index+ (ajout * power);
        }
        return index+1;
    }
    
    private int lineIndex(String lineName){
        try{
            int line=Integer.parseInt(lineName);
            return line;
        }catch(Exception e){
            return 0;
        }
    }
    
    private void setAreaStart(String cellName){
        String columnName=getColumnName(cellName);
        String lineName=getLineName(cellName);
        column_start=columnIndex(columnName);
        row_start=lineIndex(lineName);
    }

    private void setAreaEnd(String cellName){
        String columnName=getColumnName(cellName);
        String lineName=getLineName(cellName);
        column_end=columnIndex(columnName)+1;
        row_end=lineIndex(lineName)+1;
    }

    public int countColumn(){
        return (column_end-column_start);
    }
    public int countRow(){
        return (row_end-row_start);
    }
}

Puis nous avons AreaGrid qui permet de collecter les AreaCell des composants afin de constituer la grille complète :

public class AreaGrid {
    private AreaCell screen;
    private LinkedHashMap<String, AreaCell> area=new LinkedHashMap<String, AreaCell>();
    private static final String AREA_ITEM="item";

    public AreaGrid(String areaStart, String areaEnd){
        screen=new AreaCell(areaStart,areaEnd);
    }

    public String addArea(String areaStart, String areaEnd){
        String areaName=AREA_ITEM+"_"+areaStart+"_"+areaEnd;
        AreaCell areaItem=new AreaCell(areaStart, areaEnd);
        area.put(areaName,areaItem);
        return areaName;
    }

    public String getCss(){
        StringBuilder css=new StringBuilder();
        css.append("body {").append(Ascii.CRLF);
        css.append("    display: grid;").append(Ascii.CRLF);
        css.append("    grid-template-columns:"+screen.countColumn()+"fr;").append(Ascii.CRLF);
        css.append("    grid-template-rows:repeat("+screen.countRow()+",1fr);").append(Ascii.CRLF);
        css.append("}").append(Ascii.CRLF);
        for(String areaName:area.keySet()){
            AreaCell areaItem=area.get(areaName);
            css.append("."+areaName+"{").append(Ascii.CRLF);
            css.append("    grid-column:"+areaItem.column_start+"/"+areaItem.column_end+";").append(Ascii.CRLF);
            css.append("    grid-row:"+areaItem.row_start +"/"+areaItem.row_end+";").append(Ascii.CRLF);
            css.append("}").append(Ascii.CRLF);
        }
        return css.toString();
    }
}

Pour la gestion de toutes les AreaCell, nous utilisons une LinkedHashMap (ligne 3) qui permet de conserver l’ordre d’insertion, ce qui ne serait pas le cas avec une Hashtable.

Gestion des tags du modèle ScreenModel

Nous pouvons maintenant créer HtmlEngine qui délègue à AreaGrid le positionnement des éléments. Nous créons des class css qui donne un nom pour chaque areaCell et l’affecte au composant.

Création des méthodes de HtmlEngine :

HtmlEngine fournit les méthodes adaptées pour chaque Tag. Au fur et à mesure de l’utilisation de cet outil, nous ajouterons de nouveaux Tag. Pour notre page Menu, nous avons besoin de Screen, Label et Button :

public class HtmlEngine extends HtmlRepositoryEngine {

    private AreaGrid areaGrid;


    public HtmlEngine() {
        super();
    }

    protected String getHtmlBody(){
        StringBuilder body=new StringBuilder();
        body.append("<body>");
        body.append(htmlBody);
        body.append("</body>");
        return body.toString();
    }

    protected String getHtmlStyle(){
        StringBuilder style=new StringBuilder();
        style.append("<style>");
        style.append(css);
        style.append("</style>");
        return style.toString();
    }

    protected String getHtmlScript(){
        StringBuilder script=new StringBuilder();
        script.append("<script>");
        script.append(javascript);
        script.append("</script>");
        return script.toString();
    }
    /**
     * newPage(String name, String sizeStart, String sizeStop)
     */
    public boolean screen_start(RepositoryItem tag) {
        try {
            System.out.println("AppLog "+this.getClass().getName()+".screen_start"+" tag=" + tag);
            String theScreenName = tag.getName();//le nom de l'écran
            String areaStart = (String) tag.getProperty(KeyWord.AREA_START);
            String areaEnd = (String) tag.getProperty(KeyWord.AREA_END);
            System.out.println("AppLog "+this.getClass().getName()+".screen_start"+" theScreenName=" + theScreenName+" areaStart=" + areaStart+" areaEnd=" + areaEnd);
            areaGrid =new AreaGrid(areaStart,areaEnd);
            title=theScreenName;
            //pas besoin de code html pour l'instant
            //StringBuilder screenHtml=new StringBuilder();
            //htmlBody.append(screenHtml).append(Ascii.CRLF);
            StringBuilder screenCss=new StringBuilder();
            screenCss.append("*"+"{").append(Ascii.CRLF);
            screenCss.append("    box-sizing:border-box;").append(Ascii.CRLF);
            screenCss.append("}").append(Ascii.CRLF);
            screenCss.append("body"+"{").append(Ascii.CRLF);
            screenCss.append("    background-color:#AAA;").append(Ascii.CRLF);
            //screenCss.append("    justify-content:space-evenly;").append(Ascii.CRLF);
            //screenCss.append("    align-content:end;").append(Ascii.CRLF);
            screenCss.append("    grid-gap:2rem;").append(Ascii.CRLF);
            screenCss.append("}").append(Ascii.CRLF);
            css.append(screenCss);
        } catch (Exception e) {
            System.out.println("AppLog "+this.getClass().getName()+".screen_start"+" ERROR :" + e);
        }
        return false;
    }

    /*
     * endPage()
     */
    public boolean screen_finish(RepositoryItem tag) {
        try {
            //htmlBody.append("</div>").append(Ascii.CRLF);
            System.out.println("AppLog "+this.getClass().getName()+".screen_finish"+" tag=" + tag);
            css.append(areaGrid.getCss());
        } catch (Exception e) {
            System.out.println("AppLog "+this.getClass().getName()+".screen_finish"+" ERROR :" + e);
        }
        return false;
    }


    /**
     * label(String name, String caption, String sizeStart, String sizeStop)
     * <div class="item_A1_A1">Menu</div>
     */
    public boolean label_start(RepositoryItem tag) {
        try {
            System.out.println("AppLog "+this.getClass().getName()+".label_start"+" tag=" + tag);
            String name = tag.getName();//le nom de l'écran
            String caption = (String) tag.getProperty(KeyWord.CAPTION);
            String areaStart = (String) tag.getProperty(KeyWord.AREA_START);
            String areaEnd = (String) tag.getProperty(KeyWord.AREA_END);
            System.out.println("AppLog "+this.getClass().getName()+".label_start"+" name=" + name+" caption=" + caption+" areaStart=" + areaStart+" areaEnd=" + areaEnd);
            String areaClass= areaGrid.addArea(areaStart,areaEnd);
          
            StringBuilder labelHtml=new StringBuilder();
            labelHtml.append("<div class=");
            labelHtml.append(Ascii.QUOTE);
            labelHtml.append(areaClass);
            labelHtml.append(Ascii.QUOTE);
            labelHtml.append(">");
            labelHtml.append(caption);
            labelHtml.append("</div>");
            htmlBody.append(labelHtml).append(Ascii.CRLF);
        } catch (Exception e) {
            System.out.println("AppLog "+this.getClass().getName()+".label_start"+" ERROR :" + e);
        }
        return false;
    }

    public boolean label_finish(RepositoryItem tag) {
        try {
            System.out.println("AppLog "+this.getClass().getName()+".label_finish"+" tag=" + tag);
        } catch (Exception e) {
            System.out.println("AppLog "+this.getClass().getName()+".label_finish"+" ERROR :" + e);
        }
        return false;
    }

    /**
     * button(String name, String caption, String sizeStart, String sizeStop, String actionName, String actionParam, boolean editable)
     * <button type="button" class="item_A3_A3" onclick="call-screen('mon-compte.asp')">
     * mon compte : veuillez vous identifier
     * </button>
     */
    public boolean button_start(RepositoryItem tag) {
        try {
            System.out.println("AppLog "+this.getClass().getName()+".button_start"+" tag=" + tag);
            String name = tag.getName();
            String caption = (String) tag.getProperty(KeyWord.CAPTION);
            String actionName = (String) tag.getProperty(KeyWord.ACTION_NAME);
            String actionParam = (String) tag.getProperty(KeyWord.ACTION_PARAM);
            String areaStart = (String) tag.getProperty(KeyWord.AREA_START);
            String areaEnd = (String) tag.getProperty(KeyWord.AREA_END);
            System.out.println("AppLog "+this.getClass().getName()+".button_start"+" name=" + name+" caption=" + caption+" actionName=" + actionName+" actionParam=" + actionParam+" areaStart=" + areaStart +" areaEnd=" + areaEnd);
            String areaClass= areaGrid.addArea(areaStart,areaEnd);
          
            StringBuilder buttonHtml=new StringBuilder();
            buttonHtml.append("<button");

            buttonHtml.append(" type=");
            buttonHtml.append(Ascii.QUOTE);
            buttonHtml.append("button");
            buttonHtml.append(Ascii.QUOTE);

            buttonHtml.append(" class=");
            buttonHtml.append(Ascii.QUOTE);
            buttonHtml.append(areaClass);
            buttonHtml.append(Ascii.QUOTE);

            buttonHtml.append(" onclick=");
            buttonHtml.append(Ascii.QUOTE);
            buttonHtml.append(actionName);
            buttonHtml.append("('");
            buttonHtml.append(actionParam);
            buttonHtml.append("')");
            buttonHtml.append(Ascii.QUOTE);

            buttonHtml.append(">").append(Ascii.CRLF);
            buttonHtml.append(caption).append(Ascii.CRLF);
            buttonHtml.append("</button>");
            htmlBody.append(buttonHtml).append(Ascii.CRLF);;
        } catch (Exception e) {
            System.out.println("AppLog "+this.getClass().getName()+".button_start"+" ERROR :" + e);
        }
        return false;
    }

    public boolean button_finish(RepositoryItem tag) {
        try {
            System.out.println("AppLog "+this.getClass().getName()+".button_finish"+" tag=" + tag);
        } catch (Exception e) {
            System.out.println("AppLog "+this.getClass().getName()+".button_finish"+" ERROR :" + e);
        }
        return false;
    }
}

HtmlEngine va construise la page html à partir du descriptif contenu dans le code java de la class Menu.Nous avons bien un générateur de page html à parti d’un langage de description. Cela correspond à notre objectif de construire un Specific Domain Language.

En complétant HtmlEngine en fonction de nos besoin, nous pouvons créer des pages dynamiques dans Android Studio afin de créer des interfaces Utilisateurs (UI) de qualité.

Intégrer la gestion des pages dynamiques à notre projet

Nous allons maintenant intégrer notre développement à notre projet Hello Android Studio. L’idée est de pouvoir définir les pages dynamiques avec une extension ‘.asp‘ afin de pouvoir appeler ces pages qui seront construites dynamiquement lors de l’appel de la page.

Le principe est d’attraper la page demandée sur la base de l’url ‘maPage.asp’, puis de réaliser le traitement suivant :

  • identifier le nom de la page par l’url demandé [par exemple l’url est ma-page.asp]
  • charger la classe MaPage
  • construire le Repository par la méthode build(PlugsetMessage)
  • projeter le repository construit par la methode broacast(HtmlEngine)
  • récupérer le html sous la forme d’une String.

Le passage de l’url au nom de la classe se fait aussi par l’introspection. Nous avons une classe ScreenCatalogue qui permet de trouver la classe défissant la page ‘.asp’ correspondante de l’application. Les pages sont des classes qui implémentent le ScreenModel.

Création de ScreenCatalog.java :

public class ScreenCatalog {

    private ConcurrentHashMap<String, ScreenModel> cacheMap=new ConcurrentHashMap<>();
    private static final String PAGE_PATH="com.monapplicationmobile.hellowebview.pages";

    public ScreenModel get(String pageName) {
        ScreenModel page = cacheMap.get(pageName);
        if (page == null) {
            ScreenModel newPage = loadModel(pageName);
            page = cacheMap.putIfAbsent(pageName, newPage);
            if (page == null) {
                // put succeeded, use new value
                page = newPage;
            }
        }
        return page;
    }

    /**
     * on donne le nom de la page sous la forme ma-page, et on charge la classe MaPage
     * depuis un chemin de package réservé pour les pages de l'application.
     * @param pageName
     * @return
     */
    private ScreenModel loadModel(String pageName){
        try{
            String classFullName=PAGE_PATH+getClassName(pageName);
            Class pageClass=Class.forName(classFullName);
            ScreenModel maPage=(ScreenModel)pageClass.newInstance();
            return maPage;
        }catch(Exception e){
            return null;
        }
    }

    private String getClassName(String pageName){
        StringBuilder className=new StringBuilder();
        StringTokenizer words=new StringTokenizer(pageName,"-");
        while(words.hasMoreElements()){
            String word=words.nextToken();
            String firstLetter=word.substring(0,1);
            String afterLetters=word.substring(1);
            className.append(firstLetter.toUpperCase());
            className.append(afterLetters.toLowerCase());
        }
        return className.toString();
    }
}

Nous utilisons la même logique que ce que nous avons vu dans lnotre article précédant expliquant comment utiliser WebView pour afficher des pages html dans une application Android.

Dans AppWebViewClient nous complétons la méthode shouldInterceptRequest(WebView view, WebResourceRequest request) :

   @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request){
        String urlString=request.getUrl().toString();
        WebResourceResponse response = null;
        try {
            URL url = new URL(urlString);
            //System.out.println("AppLog.AppWebViewClient.shouldInterceptRequest : urlString="+urlString);
            if(  url.getProtocol().startsWith("http") && url.getHost().equalsIgnoreCase("127.0.0.2")) {
                AppWebResponse webResponse = AppLocalResource.localFileCall(url.getPath().substring(1), currentContext);
                response = new WebResourceResponse(webResponse.contentType, null, new ByteArrayInputStream(webResponse.bodyData));
            }
        }
        catch (Exception e){
            // nothing
        }
        return(response);
    }

Nous créons une classe AndroidServerPage qui va regrouper l’enchainement des traitements pour créer dynamiquement les pages web ‘.asp‘. Une méthode dédiée à AppLocalResource identifie les pages devant être gérées par Android Server Page.

  @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request){
        String urlString=request.getUrl().toString();
        WebResourceResponse response = null;
        try {
            URL url = new URL(urlString);
            //System.out.println("AppLog.AppWebViewClient.shouldInterceptRequest : urlString="+urlString);
            if(  url.getProtocol().startsWith("http") && url.getHost().equalsIgnoreCase("127.0.0.2")) {
                if (url.getPath().endsWith(".asp")) {
                    AppWebResponse webResponse = AppLocalResource.aspCall(url.getPath().substring(1), currentContext);
                    response = new WebResourceResponse(webResponse.contentType, null, new ByteArrayInputStream(webResponse.bodyData));
                } else {
                    AppWebResponse webResponse = AppLocalResource.localFileCall(url.getPath().substring(1), currentContext);
                    response = new WebResourceResponse(webResponse.contentType, null, new ByteArrayInputStream(webResponse.bodyData));
                }
            }
        }
        catch (Exception e){
            // nothing
        }
        return(response);
    }

La méthode aspCall de la class AppLocalResource va gérer le traitement dynamique de la page :

public static AppWebResponse aspCall(final String filePath, final Context currentContext) {
  try {
    String filePath2 = filePath;
    if (filePath2.startsWith(".")) filePath2 = filePath2.substring(1);

    AndroidServerPage androidServerPage=new AndroidServerPage(filePath2,currentContext);
    androidServerPage.build(null);
    androidServerPage.convertToHtml();
    byte[] bytePage=androidServerPage.getHtml().getBytes();

    String mimeType = "text/html";

    return(new AppWebResponse(200, mimeType, bytePage, ""));
  } catch (Exception e) {
    return(new AppWebResponse(404, "", null, ""));
  }
}

La classe AndroidServerPage va construire la page html demandée :

public class AndroidServerPage {

    private String pageWanted;
    private PlugsetMessage pageMessage;
    private String pageHtml;
    private Context context;

    public AndroidServerPage(String pageAsp, Context context){
        this.context=context;
        //System.out.println("AppLog.AndroidServerPage : pageAsp="+pageAsp);
        StringTokenizer fullPageName=new StringTokenizer(pageAsp,".");
        this.pageWanted=fullPageName.nextToken();
        //System.out.println("AppLog.AndroidServerPage : this.pageWanted="+this.pageWanted);
    }

    /**
     * on prend le nom de la pageWanted, on trouve la class de la page, on construit le build, on obtient le repository de la page
     * @param message
     */
    public void build(PlugsetMessage message){
        try {
            //System.out.println("AppLog.AndroidServerPage.build : message=" + message);
            ScreenCatalog catalog = new ScreenCatalog(context);
            //System.out.println("AppLog.AndroidServerPage.build : catalog=" + catalog);
            ScreenModel screenModel = catalog.get(pageWanted);
            //System.out.println("AppLog.AndroidServerPage.build : screenModel=" + screenModel);
            pageMessage = screenModel.build(message);
            //System.out.println("AppLog.AndroidServerPage.build : pageMessage=" + pageMessage);
        }catch(Exception e){
            System.out.println("AppLog.AndroidServerPage.build : ERROR " + e);
            e.printStackTrace();
        }
        //resultat : message.put("page","xml",getRepository());
    }

    private Repository getPageRepository(){
        return (Repository)pageMessage.get("page","xml");
    }

    /**
     * on prend le repository de la page, et on le converti en html
     */
    public void convertToHtml(){
        //System.out.println("AppLog.AndroidServerPage.convertToHtml : début");
        HtmlEngine htmlEngine=new HtmlEngine();
        //System.out.println("AppLog.AndroidServerPage.convertToHtml : htmlEngine="+htmlEngine);
        Repository pageRepository=getPageRepository();
        //System.out.println("AppLog.AndroidServerPage.convertToHtml : pageRepository="+pageRepository);
        String pageXml=pageRepository.getXmlString();
        //System.out.println("AppLog.AndroidServerPage.convertToHtml : pageXml="+pageXml);
        pageRepository.broadcast(htmlEngine);
        pageHtml=htmlEngine.getHtml();
        //System.out.println("AppLog.AndroidServerPage.convertToHtml : pageHtml="+pageHtml);
    }

    public String getHtml(){
        return pageHtml;
    }
}

C’est la class AndroidServerPage qui permet de créer des pages dynamiques avec Android Studio afin de construire des Interfaces Utilisateur (UI) de qualité.

Mise en oeuvre et résultat

Nous modifions la page index de l’application pour accéder à la page Menu :

<!DOCTYPE html>
<html>
<head>
    <title>Page hello webview</title>
</head>
<body>
Hello WebView Android!<br>

<button type="button" onclick="location.href='menu.asp'">
    Menu de l'application en ASP
</button>

</body>
</html>

Nous demandons à la ligne 9 au bouton d’appeler menu.asp.

Notre première execution a rencontré une erreur de sécurité.

Erreur de sécurité : err_cleartext_not_permitted

Le message d’erreur err_cleartext_not_permitted, empêche notre page de s’afficher :

WebView et AndroidServerPage - Etape 1 - usesCleartextTraffic pour corriger err_cleartext_not_permitted
WebView et AndroidServerPage – Etape 1 – usesCleartextTraffic pour corriger err_cleartext_not_permitted

Pour corriger l’erreur, nous devons ajouter des droits pour les requêtes http.

Nous ajoutons la ligne android:usesCleartextTraffic= »true » dans le fichier AndroidManifest.xml. Cela permet d’autoriser les requêtes http simple et non cryptées.

Modification de la configuration d’AndroidManifest.xml :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.monapplicationmobile.hellowebview">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:usesCleartextTraffic="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.HelloWebview"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Nous avons ajouté la ligne 10 :
android:usesCleartextTraffic= »true »

Suppression du bandeau HelloWebView

Nous souhaitons avoir une page Web prenant toute la place sur l’écran. Nous allons ajouter un paramètre dans le thème de l’application.

Modification du fichier res>values>themes>themes.xml :

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.HelloWebview" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <item name="windowNoTitle">true</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

Nous avons ajouté la ligne 14 : <item name= »windowNoTitle »>true</item>

WebView et AndroidServerPage - Etape 2 - Supprimer la barre de titre pour donner toute la place au WebView
WebView et AndroidServerPage – Etape 2 – Supprimer la barre de titre pour donner toute la place au WebView

La desciption du theme fait parti des resources res (1). La ligne donne la propriété windowNoTitle (2). Le simulateur affiche la page sans le titre (3).

La page Menu générée par notre Android Serveur Page

Le bouton sur notre page index permet d’afficher la page Menu :

WebView et AndroidServerPage - Etape 3 - La page Menu générée par notre Android Server Page
WebView et AndroidServerPage – Etape 3 – La page Menu générée par notre Android Server Page

La page que nous avons créé a pour but de montrer comment nous pouvons générer à la volée des pages HTML pour construire dans une application Android des interfaces utilisateurs. LA page Menu est une page statique et ne présente pas en l’état d’autre intérêt que de valider les étapes de création de page à partir d’un Specific Domain Langage.

Nous devons maintenant utiliser toute la puissance de cet outil pour créer des pages dynamiques. Ce sera ainsi l’objet de notre article suivant.

En conclusion

Nous avons montré que nous pouvons créer des pages dynamiques avec Android Studio pour des Interfaces Utilisateurs (UI) de qualité.

Cela va nous permettre de faire le lien avec la gestion des données de l’application. La gestion des données dans le terminal sera l’objet de notre prochain article. Nous créerons des outils de gestion de données de type NoSQL appelés Questionnaire.

Si vous avez aimé l'article vous êtes libre de le partager :-)

Laisser un commentaire