samedi 4 février 2012

Création de vues personnalisées sur Android




Ce post fait suite à celui de mon ami et confrère Yakhya Dabo qui traite de la création de vues sur Android. Après lecture de son article, vous aurez compris qu’il est préférable d’utiliser sans modération des ressources et des ficher XML pour la création des vues.  Cependant, j’ai pensé qu’il peut être intéressant de rajouter à l’analyse de mon ami, une technique souvent utilisée par les développeurs pour créer des vues personnalisées.
Cette méthode consiste à définir la classe java qui implémente la vue directement dans le fichier XML représentant cette dernière puis d’utiliser le LayoutInflater pour l’instancier.

Vous vous dites sûrement que c’est bien beau tout ce discours mais à quoi ça peut bien servir ?

D’une part, outre le fait de dissocier la représention graphique de son implémentation, cette technique permet de faciliter le découplage entre les différentes classes de votre application. En effet, la classe qui va créer la vue n’a pas forcément besoin de connaître celle qui l’implémente du moment que cette dernière respecte le contrat définit à l’aide d’une interface  java (voir exemple).


D’autre part, cela nous permet de visualiser avec l’éditeur graphique d’Eclipse la vue telle qu’elle serait affichée sur l’appareil mobile en utilisant les méthodes isInEditMode() et onFinishInflate() pour éviter des erreurs d’initialisation de la vue en fonction du contexte.

.

Par ailleurs, il faut aussi noter que cette façon de faire n’empêche en rien de faire usage de l’include pour l’insertion de notre vue dans une autre composition.

Pour illustrer ce concept, je vous propose de jeter un coup d’œil sur l’exemple suivant qui permet de créer une boite de dialogue personnalisée afin d’afficher différents types de messages d’erreur :



  1. Le contrat définissant l’affichage d’un message d’erreur :

/**
 * Base interface for a message shower
 * @author ndongo
 *
 */
public interface MessageShower {
      
       /**
        * The listener of the user response
        */
       public static interface MessageShowerListener{
             /**
              * Call back of the confirmation button
              */
             public void onConfirm();
             /**
              * Call back of the cancel button
              */
             public void onCancel();
       }
      
       /**
        * The type of the message to show
        * @author ndongo
        *
        */
       public static enum MessageType{
             NetworkConnectionFailed,
             DataBaseConnectionFailed,
             // others type of messages...
       }
      
       /**
        * Sets the type of the message to show
        * @param type the type
        */
       public void setType(MessageType type);
      
       /**
        * Sets the listener
        * @param listener the listener
        */
       public void setListener(MessageShowerListener listener);
}


  1. La vue en XML dans le répertoire res/layout (avec alternative ou pas):

<?xml version="1.0" encoding="utf-8"?>
<fr.yayandongo.articles.SimpleMessageShower xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/showerror_background" >

    <TextView
        android:id="@+id/messageTv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/margin_top_msg_text"
        android:gravity="center"
        android:singleLine="false"
        android:text="@string/message_on_graphic_editor"
        android:textColor="@color/red"
        android:textSize="@dimen/text_nomal" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/messageTv"
        android:layout_marginTop="30dp"
        android:gravity="center_horizontal" >

        <Button
            android:id="@+id/confirmBtn"
            android:layout_width="@dimen/msg_btn_width"
            android:layout_height="@dimen/msg_btn_height"
            android:text="@string/msg_confirm_btn"
            android:textSize="@dimen/text_nomal" />

        <Button
            android:id="@+id/cancelBtn"
            android:layout_width="@dimen/msg_btn_width"
            android:layout_height="@dimen/msg_btn_height"
            android:layout_marginLeft="@dimen/margin_between_msg_btns"
            android:layout_toRightOf="@+id/confirmBtn"
            android:text="@string/msg_cancel_btn"
            android:textSize="@dimen/text_nomal" />
    </RelativeLayout>

</fr.yayandongo.articles.SimpleMessageShower>

  1. La classe qui implémente la vue :

/**
 * A Simple message shower
 * @author ndongo
 *
 */
public class SimpleMessageShower extends RelativeLayout implements MessageShower {
      
       private TextView msgTv = null;
       private Button confrimBtn = null;
       private Button cancelBtn = null;
      
       private MessageShower.MessageShowerListener listener = null;

       /**
        * @param context
        */
       public SimpleMessageShower(Context context) {
             super(context);
       }

       /**
        * @param context
        * @param attrs
        */
       public SimpleMessageShower(Context context, AttributeSet attrs) {
             super(context, attrs);
       }

       /**
        * @param context
        * @param attrs
        * @param defStyle
        */
       public SimpleMessageShower(Context context, AttributeSet attrs, int defStyle) {
             super(context, attrs, defStyle);
       }
      
       @Override
       protected void onFinishInflate() {
            
             if (isInEditMode()) {
                    return;
             }
            
             // initialize the view
             msgTv = (TextView) findViewById(R.id.messageTv);
            
             confrimBtn = (Button) findViewById(R.id.confirmBtn);
             confrimBtn.setOnClickListener(new OnClickListener() {
                   
                    @Override
                    public void onClick(View v) {
                           notifyListener(true);
                    }
             });
            
             cancelBtn = (Button) findViewById(R.id.cancelBtn);
             cancelBtn.setOnClickListener(new OnClickListener() {
                   
                    @Override
                    public void onClick(View v) {
                           notifyListener(false);
                    }
             });
            
       }
      
       private void notifyListener(boolean isConfirmed){
             if (listener == null) {
                    return;
             }
            
             if (isConfirmed) {
                    listener.onConfirm();
             } else {
                    listener.onCancel();
             }
       }

       /**
        * @see MessageShower#setType(MessageType type)
        */
       @Override
       public void setType(MessageType type) {
             switch (type) {
             case DataBaseConnectionFailed:
                    msgTv.setText(R.string.message_database_failed);
                    break;
             case NetworkConnectionFailed:
                    msgTv.setText(R.string.message_network_failed);
                    break;
             default:
                    msgTv.setText(R.string.message_default);
                    break;
             }
            
       }
      
       /**
        * @see MessageShower#setListener(MessageShowerListener listener)
        */
       @Override
       public void setListener(MessageShowerListener listener) {
             this.listener = listener;
       }
}


  1. L’instanciation et l’utilisation de la vue dans une autre classe :

protected Dialog onCreateDialog(int id) {
       if (id == ERROR_DIALOG) {
             final Dialog dialog = new Dialog(this, R.style.errorShowerDialog);
                    final View msgShower = LayoutInflater.from(this).inflate(R.layout.message_shower, null);
                    ((MessageShower) msgShower).setListener(new MessageShower.MessageShowerListener(){

                           @Override
                           public void onConfirm() {
                                  dialog.dismiss();
                                  // process some thing...
                           }

                           @Override
                           public void onCancel() {
                                  dialog.dismiss();
                                  // process some thing...
                           }
                          
                    });
                   
                    //... depending on the process, we set the message type
                    // for the example lets use Network failed
                    ((MessageShower) msgShower).setType(MessageType.NetworkConnectionFailed);
                   
                    dialog.setContentView(msgShower);
                    return dialog;
             }
       return super.onCreateDialog(id);
}


En résumé la plateforme Android offre un moyen clair et efficace de séparer la description des vues  et de leur implémentation avec l’utilisation de l’XML. Il est ainsi dommage de ne pas en profiter pour réaliser des applications robustes et évolutives.
Dans le même cadre, le concept que nous avons traité ici repose sur cette base et a pour but  principal  de faciliter le découplage et la maintenabilité des applications.