Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Cross-plateforme : Android – Part 4 – Vues et Rotation

[new:30/05/2013]A la suite des trois précédentes parties qui ont exposé le pourquoi du choix d’Android, les grandes lignes de l’OS et la nature du concept d’Activité, tournons notre regard sur les vues et la gestion de la rotation d’écran…

Vues et Rotation

L’optique de ces billets sur Android n’est pas de faire un cours complet sur cet OS, il existe de nombreux livres traitant du sujet, mais bien de se concentrer sur les différences fonctionnelles essentielles avec les OS Microsoft que nous connaissons et avec lesquels nous devront “marier” les applications Android.

 

Cette partie 4 choisit de présenter la gestion des vue et de la rotation de l’écran car il s’agit d’un point essentiel faisant la différence entre du développement mobile et du développement PC. Sur ces derniers ce concept n’existe pas (la rotation). Et puis parler de rotation, c’est forcément parler des vues alors que l’inverse n’est pas forcément vrai… Et que seraient les applications sans les vue ? Pas grand chose !

Bien gérer le passage d’un mode portrait à un mode paysage et ce dans les deux sens est crucial sur un smartphone et aussi sur une tablette. C’est tout le côté pratique de ces machines qui s’exprime ici. La rotation est le symbole de l’UX spécifique des unités mobiles en quelque sorte.

Donc d’une part la rotation des écrans est un point essentiel qui différencie le développement pour mobile, et, d’autre part, cela implique de parler des vues. Et quant on a compris ce qu’est une Activité (partie 3) et qu’on a compris ce qu’était une vue et comment gérer sa rotation (partie 4 présente) on a presque tout compris d’Android (je dis bien “presque”).

Bien entendu nous nous intéresserons à ce mécanisme sous l’angle de Xamarin, c’est à dire en C# et .NET. Toutefois les vues s’expriment en format natif et n’ont pas réellement de lien avec le langage de programmation. Et tout aussi évidemment, quand je parle de “mobiles” j’entends “unités mobiles”, ce qui intègre en un tout aussi bien les smartphones que les tablettes.

Un tour par ci, un tour par là…

Parce que les appareils mobiles sont par leur taille et leur format facilement manipulables dans tous les sens la rotation est une fonction intégrée dans tous les OS mobiles. Android n’y échappe pas.

Android fournit un cadre complet pour gérer la rotation au sein des applications, que l'interface utilisateur soit créée de façon déclarative en XML (nous y reviendrons plus tard) ou par le code. Dans le premier cas un mode déclaratif permet à l’application de bénéficier des automatismes de Android, dans le second cas des actions par code sont nécessaires. Cela permet un contrôle fin à l'exécution, mais au détriment d’un travail supplémentaire pour le développeur.

Que cela soit par mode déclaratif ou par programmation toutes les applications Android doivent appliquer les mêmes techniques de gestion d’état lorsque l'appareil change d'orientation. L'utilisation de ces techniques pour gérer l'état de l’unité mobile est important parce que quand un appareil Android est en rotation le système va redémarrer l'activité courante. C’est une spécificité de l’OS. Android fait cela pour rendre plus simple le chargement de ressources alternatives telles que, principalement, des mises en page et des images conçues spécifiquement pour une orientation particulière. Quand Android redémarre l'activité celle-ci perd tout état transitoire qu’elle peut avoir stocké dans ses variables, l’instance est totalement renouvelée. Par conséquent si une activité est tributaire de certains états internes, il faut absolument les persister avant le changement d’orientation pour réhydrater la nouvelle instance. L’utilisateur ne doit en aucun cas avoir l’impression de perdre son travail en cours (surtout qu’une rotation peut être involontaire).

En outre, dans certains cas une application peut aussi choisir de se retirer du mécanisme automatique de rotation pour en prendre totalement contrôle par code et gérer elle-même quand elle doit changer d’orientation.

Gérer les rotations de façon déclarative

Je reviendrais plus tard sur la gestion des ressources dans une application Android et comment s’appliquent les règles de nommage des répertoires, leur rôle, etc. Pour l’instant il vous suffit de savoir qu’Android gère deux types de ressources : les ressources au sens classiques (fichiers axml par exemple) et les “dessinables”. Les ressources contiennent les vues, les “dessinables” tout ce qui est image en général.

Dans un précédent billet de cette série je parlais du mode non vectoriel de Android par opposition à XAML et des implications de ce choix. L’une des principales est que pour garantir la qualité d’affichage des “dessinables” il est nécessaire de les fournir dans un maximum de résolutions différentes, classées dans des répertoires dont les noms suivent des conventions comme “drawable-mdpi” ou “drawable-xhdpi”, le nom du dessinable étant le même (par exemple “icon.png”).

En Xaml un seul contrôle vectoriel peut s’adapter à toutes les résolutions, ici il faudra penser à créer toutes les variantes possibles. WinRT dans une moindre mesure oblige aussi ce genre de gymnastique pour les icones.

Les ressources de mise en page

Par défaut les fichiers définissant les vues sous Android sont placés dans le répertoire Resources/layout. Ces fichiers XML (AXML) sont utilisés pour le rendu visuel des Activités. Un fichier de définition de vue est utilisé pour le mode portrait et le mode paysage si aucune mise en page spéciale n’est fournie pour gérer le mode paysage.

Si on regarde la structure d’un projet par défaut fourni par Xamarin on obtient quelque chose de ce type :

image

Dans ce projet qui définit une seule activité nous trouvons une seule vue dans Resources/layout : “main.xml” (ces fichiers utilisent aussi l’extension axml). Quand la méthode OnCreate est appelée (voir la gestion du cycle de vie du billet précédent), elle effectue le rendu de la vue définie par “main.xml” qui, dans cet exemple, déclare un bouton.

Voici le code AXML de la vue :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<Button  
    android:id="@+id/myButton"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
</LinearLayout>

 

Les habitués de XAML ne seront pas forcément déroutés : nous avons affaire à fichier suivant la syntaxe XML qui contient des balises, éventuellement imbriquées, définissant les contrôles à afficher et leurs propriétés. Ce que nous nommons par convention en XAML le “LayoutRoot” (l’objet racine, généralement une Grid) se trouve être ici un contrôle “LinearLayout” (sans nom), un conteneur simple (je reviendrais sur les conteneurs ultérieurement) qui ressemble assez à ce qu’est un StackPanel en XAML. On voit d’ailleurs dans ses propriétés la définition de son axe de déploiement ‘android:orientation=”vertical”’. On note aussi que sa hauteur et sa largeur sont en mode “fill_parent” qui correspond au mode “Stretch” de XAML. Tout cela est finalement très simple.

On remarque ensuite que cette balise LinearLayout imbrique une autre balise, Button, et là encore pas grand chose à dire. Comme sous XAML, déclarer une balise d’un type donné créé dans la vue une instance de cette dernière.

Ce qui diffère de XAML se trouve juste dans les détails de la syntaxe finalement (hors vectoriel). La façon de spécifier les propriétés est un peu différente et certaines notations sont plus “linuxienne” et moins “user friendly” qu’en XAML. Disons-le franchement, AXML est bien plus frustre que XAML. Une chose essentielle qui manque cruellement à AXML c’est le binding… Une force de .NET même depuis les Windows Forms. Il est étrange qu’une si bonne idée ne soit pas reprise partout. De base il n’y a donc pas de Binding sous Android. On pourra contourner ce problème en utilisant tout ou partie de la librairie MvvmCross qui autorise via une notation en JSon de déclarer des Bindings dans des fichiers AXML de façon très proche de la syntaxe XAML.

Une autre nuance : chaque contrôle possède un ID au lieu de porter un nom en String, mais ceux-ci sont des codes entiers peu faciles à manipuler, pour simplifier les choses une table de correspondance est gérée par Xamarin avec d’un côté des noms clairs et de l’autre les codes. Cette table est en fait un fichier “Resource.Designer.cs” qui contient des classes déclarant uniquement les noms des objets transformés en nom d’objets entiers auxquels sont affectés automatiquement un ID numérique. On peut ainsi nommer les objets comme en XAML, par un nom en string, Xamarin s’occupe de transformer tout cela en ID numérique pour Android de façon transparente. La seule contrainte est d’utiliser une syntaxe spéciale pour l’ID qui permet de spécifier une String au lieu d’un entier (une string elle-même particulière puisque c’est en réalité le nom de la ressource qui se trouvera associé à un entier), on fait ainsi référence à cette table en indiquant “@+id/myButton” la première partie explique à Android que nous utilisons une table des ID plutôt que de fixer un code entier, derrière le slash se trouve le nom en clair “myButton”.

Ce sont ces petits choses qui font les différences parfois déroutantes pour qui vient de XAML dont on (en tout cas moi au moins !) ne dira jamais assez de bien. XAML est la Rolls des langages de définition d’UI. Mais je sais aussi que beaucoup de développeurs ont du mal avec XAML et Blend qu’ils trouvent trop complexes… Et c’est une majorité. Alors finalement, ces développeurs seront peut-être heureux de découvrir AXML !

Mais tout comme XAML, AXML via Xamarin offre un designer visuel qui simplifie grandement la mise en place d’une UI et qui évite d’avoir à taper du code.

Pour revenir à notre exemple, si l’appareil est mis en mode paysage, comme rien n’est spécifié pour le gérer, l’Activité va être recrée et elle va réinjecter le même AXML pour construire la vue ce qui donnera un affichage de ce genre :

image

Ce n’est pas très joli, mais cela veut dire que par défaut, si on ne code rien de spécial, ça marche quand même…

Pour un test cela suffit, pour une application “pro” c’est totalement inacceptable bien entendu. Il va falloir travailler un peu !

Les mises en pages différentes par orientation

Le répertoire “layout” utilisé pour stocker la définition AXML de la vue est utilisé par défaut pour stocker les vues utilisées dans les deux orientations. On peut aussi renommer ce répertoire en “layout-port” si on créé aussi un répertoire “layout-land” (port pour portrait, et land pour landscape = paysage). Il est aussi possible de ne créer que “layout-land” et de laisser le répertoire “layout” sans le renommer. De cette façon une Activité peut définir simplement deux vues chacune spécifique à chaque orientation, Android saura charger celle qu’il faut selon le cas de figure.

Imaginons qu’une activité définisse un l’AXML suivant dans “layout-port” :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:text="This is portrait"
android:layout_height="wrap_content"
android:layout_width="fill_parent" />
</RelativeLayout>

 

Elle peut alors définir cette autre AXML dans “layout-land” :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:text="This is landscape"
android:layout_height="wrap_content"
android:layout_width="fill_parent" />
</RelativeLayout>

 

La seule différence se trouve dans la définition du TextView (une sorte de TextBlock XAML). Le texte affiché a été modifié dans la seconde vue :

image

C’est simple, aucun code à gérer, juste des répertoires différents et des AXML adaptés au format… Le reste est automatiquement pris en compte par Android. Ce principe, un peu rustique (puisque basé sur une simple convention de nom de répertoires) est très efficace, facile à comprendre et très souple puisque les vues portrait et paysage peuvent être carrément différentes en tout point ou juste être des copies, ou entre les deux, une modification l’une de l’autre.

Bien gérer les orientations oblige parfois à gérer deux affichages très différents pour obtenir une UX de bonne qualité. C’est plus de travail que sur un PC dont l’écran ne tourne pas…

Les dessinables

Pour tout ce qui n’est pas défini par une vue AXML, c’est à dire les “dessinables” telles que les icones et les images en général, Android qui n’est pas trop embêtant propose exactement la même gestion…

Dans ce cas particuliers les ressources sont puisées dans le répertoire Resources/drawable au lieu de Resources/layout, c’est tout.

Si on désire afficher une variante du dessinable selon l’orientation on peut créer un répertoire “drawable-land” et y placer la version spécifique pour le mode portrait (le nom de fichier restant le même).

Dès que l’orientation changera, l’Activité sera rechargée et Android ira chercher les ressources (mises en page et dessinables) dans les bons sous-répertoires correspondant à l’orientation en cours. Aucun code n’est à écrire pour le développeur une fois encore.

Bien entendu cette simplicité se paye quelque part : on se retrouve avec plusieurs fichiers AXML ou images de même nom mais ayant des contenus différents ! Je trouve personnellement cela très risqué, c’est la porte ouverte à des mélanges non détectables (sauf au runtime). Il faut donc une organisation très stricte côté Designer et beaucoup d’attention côté Développeur… De même là où une seul définition est utilisable dans tous les cas de figure en XAML (vectoriel oblige) il faudra un cycle de conception des images plus complexes sous Android. En général on utilisera Illustrator pour créer des images vectorielles qu’on pourra ensuite exporter à toutes les résolutions désirées. Mais ce n’est qu’une méthode parmi d’autres.

Gérer les vues et les rotations par code

La majorité des applications peuvent se satisfaire des modes automatiques que nous venons de voir. C’est peu contraignant et suffisamment découplé pour des mises en page parfaitement adaptées.

Mais il arrive parfois que le développeur veuille gérer lui-même le changement d’orientation.

Le premier exemple qui me vient à l’esprit serait celui d’un lecteur de PDF par exemple. Quand je lit un tel fichier sur mon smartphone ou ma tablette je trouve que ces applications gèrent très mal le changement d’orientation car en fait elles ne le gèrent pas, elles laissent les automatismes fonctionner… C’est le cas sous Android et aussi sur Surface, pas de jaloux !

J’entends par là que lorsque je lis je ne suis pas forcément une statue, je bouge. Si je suis assis, je vais changer d’accoudoir, je vais boire, si je suis au lit j’ai les bras qui fatiguent, ça tangue un peu. Et là, pof! ça change d’orientation… C’est enquiquinant. Sur ma tablette Android Acer il y a un petit bouton pour bloquer l’orientation c’est très pratique, mais cela n’existe pas sur toutes les unités mobiles.

Un programme d’affichage de PDF bien fait devrait à mon sens gérer lui-même l’orientation en offrant à l’utilisateur trois modes : automatique, portrait forcé, paysage forcé. Ca serait tellement bien.

Je ne connais pas les 800.000 applications Android par cœur ni le million sous iOS. Donc peut-être que sous iOS ou Android il existe des lecteurs PDF qui possèdent cette fonction. je n’ai pas encore eu la chance de tomber dessus (les stores en général sont assez mauvais question recherche, même chez Google le roi des moteurs de recherche il y aurait des effort à fournir).

Bref, vous voyez maintenant à quoi peut servir la gestion “manuelle” de l’orientation, le mode automatique n’étant tout simplement pas une panacée satisfaisant toutes les utilisations possibles.

Mais il existe une autre raison qui peut obliger à gérer l’orientation par code : c’est lorsque la vue elle-même est générée par code au lieu d’être définie dans un fichier AXML. En effet, dans ce dernier cas Android saura retrouver le fichier qui correspond à l’orientation, mais dans le premier cas, puisqu’aucun fichier de définition de vue n’existe, Android et ses automatismes ne peuvent rien pour vous.

Pourquoi définir une vue par code ?

Il y a des vicieux partout… Non, je plaisante. Cela peut s’avérer très intéressant pour adapter une vue très finement aux données gérées par l’application par exemple. On peut imaginer un système de gestion de fiches, les fiches pouvant être de plusieurs catégories définies par l’utilisateur. Une catégorie de fiche définira les libellés et les zones de saisie. Un tel système est très souple et très puissant, mais dès lors il n’est plus possible à la conception du logiciel de prévoir toutes les vues possibles puisque c’est l’utilisateur qui va les définir dynamiquement par paramétrage…

Une seule solution : lors de la création de l’Activité, il faut interpréter les données et le fichier de paramétrage pour créer dynamiquement la fiche à présenter à l’écran.

Ce n’est qu’un exemple, et un seul suffit à prouver que cela peut donc être nécessaire.

La création d’une vue par code

On retrouve là encore des principes présents dans XAML qui permet aussi de créer une vue uniquement par code. AXML et XAML permettent d’ailleurs de mixer les deux modes aussi.

Sous Android la décomposition est la suivante :

  • Créer un Layout
  • Modifier ses paramètres
  • Créer des contrôles
  • Modifier les paramètres (dont ceux de mise en page) de ces derniers
  • Ajouter les contrôles au Layout créé au début
  • Initialiser le conteneur de vue en lui passant le Layout

 

Rien que de très logique. On fait la même chose en XAML, et le principe était le même sous Windows Forms ou même sous Delphi de Borland … C’est pour dire si c’est difficile à aborder…

Voici un exemple de vue créée par code, elle définit uniquement un TextView (un bout de texte) à l’intérieur d’un conteneur RelativeLayout (nous verrons les conteneurs plus en détail dans une prochaine partie) :

protected override void OnCreate (Bundle bundle)
{
        base.OnCreate (bundle);
 
        // créer le "LayoutRoot", ici un conteneur RelativeLayout
        var rl = new RelativeLayout (this);
 
        // on modifie ses paramètres
        var layoutParams = new RelativeLayout.LayoutParams (
                ViewGroup.LayoutParams.FillParent,
                ViewGroup.LayoutParams.FillParent);
                rl.LayoutParameters = layoutParams;
 
        // création d'un bout de texte
        var tv = new TextView (this);
 
        // modification des paramètres du texte
        tv.LayoutParameters = layoutParams;
        tv.Text = "Programmatic layout";
 
        // On ajoute le bout texte en tant qu'enfant du layout
        rl.AddView (tv);
 
        // On définit la vue courante en passant le Layout et sa "grappe" d'objets.
        SetContentView (rl);
}
 

Une telle Activité s’affichera de la façon suivante :

image

Détecter le changement d’orientation

C’est bien, mais nous n’avons fait que créer une vue par code au lieu d’utiliser le designer visuel ou de taper du AXML. Comme on le voit sur la capture ci-dessus Android gère ce cas simple comme celui où un seul fichier AXML est défini : le même affichage est utilisé pour le mode paysage (rien à coder pour que cela fonctionne). Cela peut suffire, mais nous voulons gérer la rotation par code.

Il faut donc détecter la rotation pour prendre des décisions notamment modifier la création de la vue.

Pour ce faire Android offre la classe WindowManager dont la propriété DefaultDisplay.Rotation permet de connaitre l’orientation actuelle.

Le code de création de la vue peut dès lors prendre connaissance de cette valeur et créer la mise en page ad hoc.

Cela devient un peu plus complexe, par force :

protected override void OnCreate (Bundle bundle)
{
        base.OnCreate (bundle);
 
        // Création du Layout
        var rl = new RelativeLayout (this);
 
        // paramétrage du Layout
        var layoutParams = new RelativeLayout.LayoutParams (
                ViewGroup.LayoutParams.FillParent,
                ViewGroup.LayoutParams.FillParent);
        rl.LayoutParameters = layoutParams;
 
        // Obtenir l'orientation
        var surfaceOrientation = WindowManager.DefaultDisplay.Rotation;
 
        // création de la mise en page en fonction de l'orientation
        RelativeLayout.LayoutParams tvLayoutParams;
 
        if (surfaceOrientation == SurfaceOrientation.Rotation0 ||
                surfaceOrientation == SurfaceOrientation.Rotation180) {
                tvLayoutParams = new RelativeLayout.LayoutParams (
                        ViewGroup.LayoutParams.FillParent, 
                        ViewGroup.LayoutParams.WrapContent);
        } else {
                tvLayoutParams = new RelativeLayout.LayoutParams (
                        ViewGroup.LayoutParams.FillParent,
                         ViewGroup.LayoutParams.WrapContent);
                tvLayoutParams.LeftMargin = 100;
                tvLayoutParams.TopMargin = 100;
        }
 
         // création du TextView
        var tv = new TextView (this);
        tv.LayoutParameters = tvLayoutParams;
        tv.Text = "Programmatic layout";
 
        // ajouter le TextView au Layout
        rl.AddView (tv);
 
        // Initialiser la vue depuis le Layout et sa grappe d'objets
        SetContentView (rl);
}

 

Empêcher le redémarrage de l’Activité

Dans certains cas on peut vouloir empêcher l’Activité de redémarrer sur un changement d’orientation. C’est un choix particulier dont je ne développerai pas ici le potentiel. Mais puisque le sujet est la rotation il est bon de savoir qu’on peut bloquer la réinitialisation de l’Activité.

La façon de le faire sous Xamarin en C# est simple puisqu’elle exploite la notion d’attribut. Il suffit alors de décorer la sous classe de Activity par un attribut spécial :

[Activity (Label = "CodeLayoutActivity",
 ConfigurationChanges=Android.Content.PM.ConfigChanges.Orientation)]

 

Bien entendu le code de l’Activité doit être conçu pour gérer la situation : puisque l’activité ne sera pas recréer, sa méthode OnCreate() ne sera pas appelée… Et puisque c’est traditionnellement l’endroit où on charge ou créé la vue, cela implique de se débrouiller autrement. Cet “autrement” a heureusement été prévu, il s’agit de la méthode OnConfigurationChanged(). On comprend dès lors beaucoup mieux le contenu de la définition de l’attribut plus haut qui ne fait que demander à Android de déclencher cette méthode lorsque dans la configuration l’Orientation change…

A partir de là les portes de l’infinie des possibles s’ouvre au développeur… La vue sera-t-elle totalement rechargée, recrée par code, modifiée par code, ou seules quelques propriétés d’objets déjà présents dans la vue seront-elles adaptées… Tout dépend de ce qu’on a faire et pourquoi on a choisi de gérer le changement d’orientation de cette façon.

Cette méthode fonctionne pour les deux cas de figure possibles : vue chargée depuis un AXML ou vue créée par code.

Maintenir l’état de la vue sur un changement d’orientation

Par défaut l’Activité est donc redémarrée à chaque changement d’orientation. En clair, une nouvelle instance est construite ce qui implique que les états et données maintenues par l’activité en cours sont perdus.

Ce n’est clairement pas une situation acceptable dans la majorité des cas. L’utilisateur ne doit pas perdre son travail ni même les informations affichées sur un simple changement d’orientation d’autant que celui-ci, comme je le disais, peut être involontaire.

Là aussi tout est prévu.

Android fournit deux moyens différents permettant de sauvegarder ou restaurer des données durant le cycle de vie d’une application :

  • Le groupe d’états (Bundle) qui permet de lire et écrire des paires clé/valeur
  • Un instance d’objet pour y placer ce qu’on veut (ce qui peut s’avérer plus fins que de simples paires clé/valeur).

 

Le Bundle est vraiment pratique pour toutes les données “simples” qui n’utilisent pas beaucoup de mémoire, alors que l’utilisation d’un objet est très efficace pour des données plus structurées et contrôlées ou prenant du temps à être obtenue (comme l’appel à un web service ou une requête longue sur une base de données).

Le Bundle

Le Bundle est passé à l’Activité par Android. L’application peut alors s’en servir pour mémoriser son état en surchargeant OnSaveInstateState().

Il y a d’autres façons d’exploiter le Bundle. Dans l’exemple qui suit l’UI est faite d’un texte et d’un bouton. Le texte affiche le nombre de fois que le bouton a été cliqué. On souhaite que cette information ne soit pas annulée par une rotation. Un tel code pourra ressembler à cela :

int c;
 
protected override void OnCreate (Bundle bundle)
{
        base.OnCreate (bundle);
 
        this.SetContentView (Resource.Layout.SimpleStateView);
 
        var output = this.FindViewById<TextView> (Resource.Id.outputText);
 
        if (bundle != null)
                c = bundle.GetInt ("counter", -1);
        else
                c = -1;
 
        output.Text = c.ToString ();
 
        var incrementCounter = this.FindViewById<Button> (Resource.Id.incrementCounter);
 
        incrementCounter.Click += (s,e) => { 
                output.Text = (++c).ToString();
        };
}

 

Le code ci-dessus ne fait que compter les clics et les afficher. La variable “c” qui est utilisée appartient à l’Activité, c’est un champ de type “int”. Lorsqu’une rotation va avoir lieu, l’Activité va être reconstruire. Le code utilise le Bundle pour y récupérer cet entier qui est stocké sous le nom de “counter”.

Simple. Mais qui a stocké la valeur de “c” sous ce nom ? C’est la surcharge de la méthode OnSaveInstateState() :

protected override void OnSaveInstanceState (Bundle outState)
{
        outState.PutInt ("counter", c);
        base.OnSaveInstanceState (outState);
}

 

Lorsque la configuration de l’état de l’Activité nécessite une sauvegarde Android exécutera cette méthode, et c’est elle qui mémorise la valeur de “c” sous le nom “counter” (la clé) dans le Bundle.

C’est ce qui permet au code OnCreate() de l’Activité vu plus haut d’exploiter le Bundle qui lui est passé pour y rechercher cette entrée et récupérer la valeur de “c”.

View State automatique

Le mécanisme que nous venons de voir est particulière simple et efficace. Mais il peut sembler contraignant d’avoir à sauvegarder et relire comme cela toutes les valeurs se trouvant dans l’UI.

Heureusement ce n’est pas nécessaire !

Toute fenêtre de l’UI (tout objet d’affichage) possédant un ID est en réalité automatiquement sauvegardée par le OnSaveInstateState().

Ainsi, si un EditText (un TextBox XAML) est défini avec un ID, le texte éventuellement saisi par l’utilisateur sera automatiquement sauvegardé et rechargé lors d’un changement d’orientation et cela sans aucun besoin d’écrire du code.

Limitations du Bundle

Le procédé du Bundle avec le OnSaveInsateState() est simple et efficace comme nous l’avons vu. Mais il présente certaines limitations dont il faut avoir conscience :

  • La méthode n’est pas appelées dans certains cas comme l’appui sur Home ou sur le bouton de retour arrière.
  • L’objet Bundle lui-même qui est passé à la méthode n’est pas conçu pour conserver des données de grande taille comme des images par exemple. Pour les données un peu “lourdes” il est préférable de passer par OnRetainNonConfigurationInstance() ce que nous verrons plus bas.
  • Les données du Bundle sont sérialisées ce qui ajoute un temps de traitement éventuellement gênant dans certains cas.

 

Mémoriser des données complexes

Comme on le voit, si l’application doit mémoriser des données complexes ou un peu lourdes il faut mettre en œuvre d’autres traitements que la simple utilisation du Bundle.

Mais cette situation aussi a été prévue…

En effet, Android a prévu une autre méthode, OnRetainNonConfigurationInstance(), qui peut retourner un objet. Bien entendu ce dernier doit être natif (si Xamarin nous offre .NET l’OS ne s’en préoccupe pas) et on utilisera un Java.Lang.Object (ou un descendant).

Il y a deux avantages majeurs à utiliser cette technique :

  • L’objet retourné par la méthode OnRetainNonConfigurationInstance() fonctionne très bien avec des données lourdes (images ou autres) ou des données plus complexes (structurées plus finement qu’en clé/valeur)
  • La méthode OnRetainNonConfigurationInstance() est appelée à la demande et seulement quand cela est utile, par exemple sur un changement d’orientation.

 

Ces avantages font que l’utilisation de OnRetainNonConfigurationInstance() est bien adaptée aussi aux données qui “coutent cher” à recharger : longs calculs, accès SGBD, accès Web, etc.

Imaginons par exemple une Activité qui permet de cherche des entrées sur Twitter. Les données sont longues à obtenir (comparées à des données locales), elles sont obtenues en format JSon,il faut les parser et les afficher dans une liste, etc. Gérer de telles données sous la forme de clé/valeur dans le Bundle n’est pas pratique du tout et selon la quantité de données obtenues le Bundle ne sera pas forcément à même de gérer la chose convenablement.

L’approche doit ainsi être revue en mettant scène OnRetainNonConfigurationInstance() :

public class NonConfigInstanceActivity : ListActivity
{
        protected override void OnCreate (Bundle bundle)
        {
                base.OnCreate (bundle);
                SearchTwitter ("xamarin");
        }
 
        public void SearchTwitter (string text)
        {
                string searchUrl = String.Format(
                        "http://search.twitter.com/search.json?" + 
                        "q={0}&rpp=10&include_entities=false&" +
                        "result_type=mixed", text);
 
                var httpReq = (HttpWebRequest)HttpWebRequest.Create (
                        new Uri (searchUrl));
                httpReq.BeginGetResponse (
                        new AsyncCallback (ResponseCallback), httpReq);
        }
 
        void ResponseCallback (IAsyncResult ar)
        {
                var httpReq = (HttpWebRequest)ar.AsyncState;
 
                using (var httpRes =
                        (HttpWebResponse)httpReq.EndGetResponse (ar)) 
                {
                        ParseResults (httpRes);
                }
                }
 
        void ParseResults (HttpWebResponse httpRes)
        {
                var s = httpRes.GetResponseStream ();
                var j = (JsonObject)JsonObject.Load (s);
 
                var results = (from result in (JsonArray)j ["results"]
                        let jResult = result as JsonObject
                        select jResult ["text"].ToString ()).ToArray ();
 
                RunOnUiThread (() => {
                        PopulateTweetList (results);
                });
        }
 
        void PopulateTweetList (string[] results)
        {
                ListAdapter = new ArrayAdapter<string> (this,
                        Resource.Layout.ItemView, results);
        }
}

 

Ce code va fonctionner parfaitement, même en cas de rotation. Le seul problème est celui des performances et de la consommation de la bande passante : chaque rotation va aller chercher à nouveau les données sur Twitter… Ce n’est définitivement pas envisageable dans une application réelle.

C’est pourquoi le code suivant viendra corriger cette situation en exploitant la mémorisation d’un objet Java :

public class NonConfigInstanceActivity : ListActivity
{
        TweetListWrapper _savedInstance;
 
        protected override void OnCreate (Bundle bundle)
        {
                base.OnCreate (bundle);
 
                var tweetsWrapper = 
                        LastNonConfigurationInstance as TweetListWrapper;
 
                if (tweetsWrapper != null)
                        PopulateTweetList (tweetsWrapper.Tweets);
                else
                        SearchTwitter ("xamarin");
        }
 
        public override Java.Lang.Object
                OnRetainNonConfigurationInstance ()
        {
                base.OnRetainNonConfigurationInstance ();
                return _savedInstance;
        }
 
 
        void PopulateTweetList (string[] results)
        {
                ListAdapter = new ArrayAdapter<string> (this,
                        Resource.Layout.ItemView, results);
                _savedInstance = new TweetListWrapper{Tweets=results};
        }
}

 

Maintenant, quand l’unité mobile est tournée et que la rotation est détectée les données affichées sont récupérées de l’objet Java (s’il existe) plutôt que déclencher un nouvel appel au Web.

Dans cet exemple les résultats sont stockés de façon simples dans un tableau de string. Le type TweetListWrapper qu’on voit plus haut est ainsi défini comme cela :

class TweetListWrapper : Java.Lang.Object
{
        public string[] Tweets { get; set; }
}

 

Comme on le remarque au passage Xamarin nous permet de gérer des objets natifs Java de façon quasi transparente.

Conclusion

L’approche choisie dans ce billet permet d’entrer dans le vif du sujet en abordant des spécificités “utiles” de Android et de Xamarin. Encore une fois il n’est de toute façon pas question de transformer le blog en un énième livre sur Android en partant de zéro. L’important est de présenter l’esprit de cet OS, ces grandes similitudes et ses différences avec les OS Microsoft que nous connaissons, et vous montrer comment on peut en C# et en .NET facilement programmer des applications pour smartphones ou tablettes tournant sous Android.

Cela s’inscrit dans la logique de la première trilogie d’articles traitement du développement cross-plateforme (et qui montrait l’utilisation de MvvmCross).

Le but est bien de satisfaire une nouvelle nécessité : être en phase avec le marché dominé par Android sur les mobiles (smartphones et tablettes) et savoir intégrer ces appareils dans une logique LOB au sein d’équipements gérer sous OS Microsoft.

L’aventure continue avec la partie 5 à venir, alors…

Stay Tuned !

blog comments powered by Disqus