Une app mobile hybride en HTML5 performante, Yes we can !!!

Actualité ekino -

Notre retour d'expérience sur la création d'une App Android HTML5 avec AngularJS et Phonegap

Article paru le :
Une app mobile hybride en HTML5 performante, Yes we can !!!
Article paru le :

Introduction / Contexte

Introduction

HTML5 et mobile ne font pas toujours bon ménage lorsqu’il s’agit de faire des applications métiers relativement complexes ou demandant un rendu graphique et motion léché.

C’est du moins ce que nos précédentes expériences mobiles autour de la technologie phare du W3C ( à base de jQuery Mobile / Pure MVC / Titanium / the-m-project / etc…) nous avaient laissé comme arrière goût.

De fortes compétences en interne en Flash et Flex avaient qui plus est parachevées d’invalider HTML5 dans le cadre de nos projets d’applications mobiles hybrides.

Mes pérégrinations webesques m’ont néanmoins rendu à l’évidence d’un fait important, il n’y a jamais de vérité parfaite et terminale concernant le bon choix technologique sur mobile. Les avis varient selon la période, les modes voir des anticyclones qui s’approchent.

Les propos de certains noms du net et des exemples probant d’intégration remettant le sujet en question, mais aussi un de nos clients ayant une aversion pour Flash, nous avons décidé qu’il était temps de remettre les pieds à l’étrier et de re-tester l’expérimentation d’HTML5 pour du mobile.

Dans le cadre d’un proto cependant afin de ne pas affecter un projet devant être mis en production.

Application témoin

Comme tout prototype qui se respecte, nous avons décidé de nous mettre dans un contexte projet réaliste. Une application métier assez lourde présentant une interface de type : “sliding panels layout” dans l’esprit de Jasmine.

Chaque panneau représentant ainsi une vue avec un principe de navigation en entonnoir.

Une petite subtilité, la liste apparaissant dans l’étape 3 devait contenir quelques éléments afin de valider les problématiques de fluidité de scroll, environ 10 000 histoire d’être sûrs :-). (on sort un peu du principe d’application réaliste mais il était très important pour nous de statuer sur ce point.)

Un petit mockup de l’application pour présenter les successions d’écrans :

xxxTouch_3

Afin de d’accentuer le côté réaliste du proto, nous nous sommes donné une contrainte de temps : ~2 jours.

J’ai ainsi clairement fait l’impasse sur une partie de l’application. Chaque vue aurait un contenu statique en dehors de la vue liste.

Nous avons priorisé en fonction des éléments nécessaire à la validation du proto :

  1. Animations
  2. Listes
  3. Vues multiples
  4. Routing

Support

Il fallait un vrai challenge à ce test , à savoir un support improbable sur une tablette complètement “outdated” avec des demandes abusives en terme de réactivité de l’interface. Seul moyen de satisfaire ma chère team leader dubitative sur cette technologie souvent qualifiée d’immature.

Choix évident compte tenu des problèmes perçus depuis le début de notre histoire avec le mobile, la tablette qui nous est apparue comme la plus sujette à nous pourrir la vie s’est trouvée être la Galaxy Tab 2 10.1.

Choix du framework

En préambule de la réalisation de ce POC, un mot d’ordre de ma hiérarchie m’avait été donné.

Utiliser un framework applicatif avec si possible des composants UI. Quelque chose de réutilisable et validé par une partie de la communauté.

Phonegap / Cordova

Côté framework de création d’app hybride, la communauté s’entendant autour de phonegap/cordova et celui-ci séant à notre problématique, nous avons plutôt mis l’accent sur le reste, le framework applicatif.

Le choix d’Angular

Angular n’était pas un mot d’ordre mais une forte probabilité, j’avais envie de me faire une idée sur ce framework dans un environnement mobile limité.

Forts de compétences en Flex, il s’agissait en effet d’un framework captant l’attention des architectes de l’équipe.

Un de nos architectes avait d’ailleurs été conquit lors de la réalisation d’une application web responsive mais nous n’avions toutefois pas encore poussé Angular sur de l’application mobile.

La grande force d’Angular, selon moi est son extraordinaire vitalité au sein de la communauté. On peut ajouter à ça les notions de data-binding et de routing qui simplifient grandement la réalisation d’applications métier.

Je ne vais pas m’étaler sur Angular car il constitue selon moi un sujet (voir plusieurs) à part entière.

Angular n’ayant pas de composants UI, j’ai cherché à lui associer un toolkit fourni à cet effet.

2 toolkits/frameworks retenaient alors mon attention :

  1. JqUI
  2. Sencha
  3. Compte tenu de nos échecs (en terme de performance) avec jQuery mobile, je ne l’avait pas mis en concurrence.

Que dire si ce n’est que j’ai essayé. Mais que rien ne m’a vraiment satisfait.

Entre récupérer un framework complet comme Sencha dont la philosophie ne me semblait que partiellement compatible avec celle d’Angular ou JqUI qui finalement ne proposait pas grand chose de plus, j’ai préféré arrêter les frais et faire quelque chose de plus custom.

J’avais donc ma base : Angular et rien d’autres.

Je conseille pour la suite du billet la lecture d’articles sur Angular. Ce framework est assez spécifique et je vais aborder certains points très particuliers.

angular-ui/ui-router

Angular et les vues multiples

Sans rentrer trop dans les détails, Angular impose l’utilisation d’une seule vue par écran, et chaque vue est associée à une route.

Dans le cadre de mon proto, je voulais avoir plusieurs vue ayant un rapport de parent/enfant afin de respecter ma logique de navigation en entonnoir, et cela dans un écran unique.

Mon brief indiquait qui plus est que les vues devaient être animées au chargement et au déchargement de chacune d’entre elle.

Une recherche sur google : “angular multiple view” et la lecture d’un billet de blog : “http://www.bennadel.com/blog/2441-Nested-Views-Routing-And-Deep-Linking-With-AngularJS.htm” plus tard, je me retrouvait avec un choix cornélien entre deux solutions :

  1. Celle du blog : utiliser des ngSwitch
  2. Celle révélée par un des commentaires : ui-router

La solution de ngSwitch étant plus custom et celle d’ui-router plus déclarative mais plus “fermée”.

Je me suis néanmoins rapproché de la solution 2 étant donnée ma contrainte de temps et le dynamisme de ses développeurs.

UI-router lien

Cette approche déclarative permet d’associer à des états de l’application des “routes”, des “contrôleurs” et des “vues”.

Fonctionnalités du module :
Je traduis ici basiquement les propos de l’équipe en charge du projet

  • Un state manager$stateProvider and $state, pour séparer les notions d’état et de routing
  • États imbriqués (relations parent/enfant).
  • Gestion de vue multiple via un système de hommage spécifique. Directive ui-view.
  • Routing d’URL
  • Etc…

Une démo est disponible ici : http://angular-ui.github.io/ui-router/sample/#/

Et la doc : https://github.com/angular-ui/ui-router/wiki

La mise en place de la solution, dans mon cas, chacun des états correspondant à une étape et donc un volet de mon application.

Les liens entre chacun des volets pouvaient se faire très simplement avec le système de routing.

Chaque vue étant gérée par son propre contrôleur et disponible dans son propre template HTML.

[code lang=”js”]<br />$stateProvider<br />.state(‘app’, {<br /><%%KEEPWHITESPACE%%> url: ‘/app’,<br /><%%KEEPWHITESPACE%%> views: {<br /><%%KEEPWHITESPACE%%> ” : {<br /><%%KEEPWHITESPACE%%> templateUrl: ‘partials/app.html’,<br /><%%KEEPWHITESPACE%%> controller : [‘$scope’ , ‘$stateParams’ , ‘$state’, ‘$rootScope’, Controller]<br /><%%KEEPWHITESPACE%%> },<br /><%%KEEPWHITESPACE%%> },<br />})<br />.state(‘app.userId’, {<br /><%%KEEPWHITESPACE%%> url: ‘/userId’,<br /><%%KEEPWHITESPACE%%> views: {<br /><%%KEEPWHITESPACE%%> ”: {<br /><%%KEEPWHITESPACE%%> templateUrl: ‘partials/user.html’,<br /><%%KEEPWHITESPACE%%> controller : [‘$scope’ , ‘$stateParams’ , ‘$state’, ‘$rootScope’, Controller]<br /><%%KEEPWHITESPACE%%> },<br /><%%KEEPWHITESPACE%%> }<br />})<br />.state(‘app.userId.detail’, {<br /><%%KEEPWHITESPACE%%> url: ‘/detail’,<br /><%%KEEPWHITESPACE%%> views: {<br /><%%KEEPWHITESPACE%%> ”: {<br /><%%KEEPWHITESPACE%%> templateUrl: ‘partials/user.detail.html’,<br /><%%KEEPWHITESPACE%%> controller : [‘$scope’ , ‘$stateParams’ , ‘$state’, ‘$rootScope’, Controller]<br /><%%KEEPWHITESPACE%%> },<br /><%%KEEPWHITESPACE%%> },<br />})<br />.Etc…;<br />[/code]

Et bim !!! J’avais une application avec des écrans factices fonctionnels en un temps record.

Le vrai problème de ce système de “nested views”, c’est que dans mon cas, mes vues devaient être côte à côte et non l’une dans l’autre.

Côté CSS

S’en est suivi un joyeux mic-mac du côté des feuilles de style, il fallait en effet que mes vues qui étaient inclues les unes dans les autres apparaissent les unes à côté des autres.

Je n’ai pas d’avis à donner concernant ce point là car il s’agit sans doute d’un problème de conception de notre côté.

Avoir un rapport hiérarchique entre les vues d’accord, mais pas avec un aspect visuel qui ne le reflète pas.

Ça aurait sans doute pu être simplifié par l’absence de transition au chargement et déchargement des vues mais je me devais de respecter cette partie du brief qui était primordiale pour valider la solution finale.

Un point sur les animations

Avec Angular, un changement de classe peut être bindé sur un changement du model. Il était donc simple d’introduire une animation au chargement des vues.

Au moment où une nouvelle route est détectée, le contrôleur de la vue associée est exécuté et un flag d’ouverture peut être  ajouté au modèle.

La directive ngClass nous permet alors d’ajouter une classe à l’élément DOM nouvellement chargé.

Il suffit que cette classe contienne une animation CSS3 et l’animation se fait au chargement de l’élément.

En exemple :

Contrôleur :

[code lang=”js”]<br />Ctrl = function($scope, $stateParams, $state){<br /><%%KEEPWHITESPACE%%> $scope.isOpen = true;<br />}<br />[/code]

HTML :

[code lang=”html”]<br /><div ng-scope ng-class="{animationOuverture:isOpen}"><br />[/code]

Persiste en revanche une problématique et non des moindre, animer la fermeture de l’élément.

Une modification de l’URL entraine irrémédiablement une mise à jour de l’état et donc la disparition du contenu de ma vue à fermer.

Le jeu était donc de trouver comment bloquer le changement d’état une fois celui-ci instancié et relancer le digest ANgular une fois l’animation de fermeture terminée.

J’ai trouvé une solution … sale mais qui m’a donné pleine satisfaction dans le cadre de mon POC.

Un évènement est levé sur chaque changement d’état $stateChangeStart  et il y a la possibilité de faire un preventDefault() et de bloquer l’exécution du digest. Nous avons ainsi tout ce qu’il faut pour notre “truc”.

[code lang=”js”]<br />$scope.open = true;<br />$scope.$on(‘$stateChangeStart’, function(e, next){<br /><%%KEEPWHITESPACE%%> // Dans mon cas, on vérifie que la vue est ouverte et que l’état qui suit soit bien le parent<br /><%%KEEPWHITESPACE%%> if ($scope.isOpen && next.name == previousState){<br /><br /><%%KEEPWHITESPACE%%> //block state change<br /><%%KEEPWHITESPACE%%> e.preventDefault();<br /><br /><%%KEEPWHITESPACE%%> //animate<br /><%%KEEPWHITESPACE%%> $scope.isOpen = false;<br /><br /><%%KEEPWHITESPACE%%> $timeout(function(){<br /><%%KEEPWHITESPACE%%> //relaunch state change<br /><%%KEEPWHITESPACE%%> $state.transitionTo(next.name);<br /><br /><%%KEEPWHITESPACE%%> //reset model<br /><%%KEEPWHITESPACE%%> $rootScope.$digest();<br /><br /><%%KEEPWHITESPACE%%> },$rootScope.delayClose); //temps d’éxécution de l’animation<br /><%%KEEPWHITESPACE%%> }<br />});<br />[/code]

HTML :

[code lang=”js”]<div ng-scope ng-class="{animationOuverture:isOpen, animationFermeture:!isOpen}">[/code]

À ce moment là, j’avais une application pleinement fonctionnelle, il ne me restait plus qu’à monter les vues et faire enfin un test de performance.

1/2 journée et un petit millier de lignes de CSS plus tard, mon application prenait forme, animations entre les vues et branchements compris.

Toutes les vues étant statiques mise à part la petite liste de 100 éléments pour commencer.

Une histoire de liste … virtuelle

Mise en route de l’application

Premier apk et premiers sentiments … mitigés. Le chargement de l’application est instantané. Les transitions entre les panneaux sont fluides. Tout fonctionne relativement correctement, sauf la liste. Celle-ci ne scroll tout bonnement pas sur Android (pas de problème sous chrome ni iPad cependant).

Il s’avère que la fluidité de l’animation entre les panneaux et le scroll de la liste étaient intimement liés.

Animation-fill-mode

Afin de m’assurer que l’animation applique bien les valeurs des keyframes en début et en fin d’animation à l’élément cible, j’utilisais la propriété animation-fill-mode=”forwards” dans un cas et “backwards” dans l’autre.

Il semblerait que la version de mon navigateur cible ne supportait pas bien ces propriétés et qu’il valait mieux s’en passer dans le cas de l’ouverture. ( Pour la fermeture, c’était en revanche impératif que le panneau reste caché entre la fin de l’animation et sa suppression du DOM par le digest d’Angular)

Enlever le forward à l’ouverture a tout simplement supprimé mon problème, j’avais désormais une liste fonctionnelle et scrollable. Après quelques recherche, j’ai constaté que Android ne supporte pas du tout cette propriété et qu’il arrivait quelle occasionne des comportement gênants aléatoires.

Note : La mise à jour hasardeuse de la tablette à corrigé le bug (4.0.3 > 4.0.4)

Angular Virtual Scroll

L’application était désormais terminée et fonctionnelle,  mais avec 100 éléments dans la liste. Or, je m’étais fixé 10000 afin de satisfaire toute personne dubitative.

Un pattern simple existe pour palier à ce type de problématiques, les listes virtuelles.

Le concept étant de ne charger dans le DOM que ce qui est vu et ce qui est nécessaire au scroll au dessus et en dessous de la zone visible.

Capture d’écran 2013-04-19 à 15.05.16

Le temps pressant, je me suis tourné vers un module existant en naviguant sur le très bon annuaire de modules Angular : ngmodules.org

Trois clics plus tard, j’avais trouvé ce que je cherchais, le module : angular-virtual-scroll http://blog.stackfull.com/

En terme d’implémentation, c’est très simple, il suffit de remplacer la directive ng-repeat par une de leur sauce : sf-virtual-repeat. Le tout fonctionne comme un ng-repeat.

Le résultat a été rapide et surprenant, ma liste de 10000 éléments, outre le temps de chargement du JSON associé était extrêmement fluide pour mes premiers tests sous chrome.

Il ne restait alors qu’à tester en condition réelle sous Android.

… encore une mauvaise surprise, les mobiles ont selon les versions pas mal de problème avec l’évènement scroll, hors celui-ci est à la base du fonctionnement du module. ( Le chargement dans le DOM des nouveaux éléments de liste étant “bindé” sur l’évènement scroll en fonction du scrollTop de la zone scrollable )

Une chose à savoir sur l’event scroll sur mobile est qu’il n’est pas déclenché au fûr et à mesure mais qu’à l’arrêt du scroll voir au tap sur Android ( Android <= 4.0 & iOS ). Si on ajoute à ça le “momentum”, le défilement inertiel que l’on retrouve souvent sur les périphériques mobiles, on risque d’attendre longtemps la fin du scroll.

Un bon lien  pour comprendre cette problématique : http://barrow.io/overflow-scrolling et qui fournit une démo du problème : http://barrow.io/lab/overflow/

=> iOS

Le contournement est simple bien que un petit peu décéptif, désactiver le momentum en n’ajoutant pas à l’élément qui a l’overflow la propriété : -webkit-overflow-scrolling: touch.

À cela, il faut modifier légèrement le script pour qu’il mette à jour le scroll pendant l’événement “touchmove” de scroll de la liste.

iOS7 corrigera enfin ce problème et le workaround ne sera plus nécessaire.

=> Android

Le problème a été traité dans Jelly Bean 4.1, plus besoin de faire un touch sur la zone pour générer l’event scroll.

Merci Samsung, une nouvelle mise à jour (2 en 2 jours 😉 ) ayant apporté Jelly Bean à la galaxy tab, je n’avais plus de problème majeur de scroll sur Android.

J’espère néanmoins que ces informations seront utiles à d’autres.

Conclusion

Durant ces quelques jours de développement, je suis passé par différents stades.

La communauté Angular m’a surpris par sa réactivité et par la qualité des modules que j’ai trouvé ici et là.

J’ai découvert des bugs improbables souvent déjà pris en compte par des frameworks que je m’était imposé de ne pas utiliser.

Nous avons ainsi validé notre prototype. Angular est taillé pour le mobile et la tablette, et HTML5 est viable si on s’affranchit notamment de jQuery mobile dans un contexte d’application hybride.

Bien que nos idées ne changent pas du tout au tout concernant l’utilisation de HTML5 vs. Air mobile pour de l’hybride, elles ont sacrément évolué et je pense que nous sommes prêt à retenter l’expérience si le sujet s’y prête.

Les bons points de l’expérience :

  • Le framework Angular m’a donné pleine satisfaction pour du mobile
  • Les listes ne font plus peur, on peut avoir quelque chose de fluide, même avec un nombre d’éléments indécent.
  • On a invalidé un thèse récurrente en interne selon laquelle HTML5 ne pouvait pas faire d’application hybride lourde mais fluide.

Les éléments qui peuvent être améliorés d’après moi :

  • La gestion des animations entre les vues me semble un peu “tricky” à mettre en place et pas forcément pérenne.
  • L’interface est fluide mais pourrait sans doute l’être encore plus (on atteins toutefois les limitations de la tablette)
  • Encore trop de différences entre les navigateurs (et les webviews qui en découlent) qui occasionnent bugs et difficulté de prise en main.

Code et démo de l’application

https://github.com/florianharmel/angular-app

Pour la suite

Pour aller plus loin, je pense qu’il faudrait :

  • “Brancher” toute l’application et voir ce qu’on en ressort.
  • Ajouter la couche tactile à l’interface pour la gestion des “gesture”
  • Porter l’application sur mobile ( j’ai commencé mais ce n’est pas encore parfait )
Commentaires (5)
  1. Kriss

    Et du coup , vous n’avez pas essayé phonegap alors ?
    Ou du moins comparer les 2 ?
    Cordialement

    1. Florian

      Kriss, deux mois pour répondre désolé.

      Ce que j’essaie de dire dans l’article c’est que pour faire une App hybride en HTML5, la coquille employée est la plupart du temps Cordova/Phonegap.

      Phonegap n’est pas à proprement parler un framework mais juste un “layer” entre une application HTML5 et le périphérique
      Il n’y a pas vraiment de concurrents à Phonegap si ce n’est Titanium qui lui offre une approche totalement différente.

      Dans mon article j’ai plutôt mis l’accent sur le framework UI / RIA qui est ici Angular car à mon sens c’est de ce côté là qu’il y a des tests à faire.

      Mais pour résumer, l’application est développée en Angular et déployée sur le device en utilisant le jeu d’API de Cordova/Phonegap.

      Florian

  2. Jean

    Bonjour,

    Un grand merci pour ce travail accompli, qui bénéficie à tous les développeurs front-end qui, comme moi, essayent de faire un minimum de veille et cherchent à connaître les meilleurs usages en termes de création d’application hybride.

    Seulement, j’arrive sur cette page web 3 ans après la bataille… Aussi, la seule question qui surgit en ce vendredi 10 juin 2016 est : qu’est-ce qui a évolué depuis 3 ans en termes d’usage, de recommandations, de technos (de préférence open source) liées au développement d’applications dites hybrides ?

    Recommandez-vous toujours la double solution AngularJS + PhoneGap ?

  3. Fabien Raynaud

    Fabien Raynaud

    Bonjour Jean et merci pour vos retours !

    Pour répondre à vos attentes : il serait dur, dans un commentaire, de faire un retour 3 ans après sur tout ce qui a changé. Néanmoins, certaines solutions sortent du panier aujourd’hui, en fonction des besoins : Ionic (qui se base sur Angular + Cordova + une librairie de composants) et bien sûr React Native (qui se base sur la technologie React et qui permet le pilotage des composants natifs depuis le JavaScript).

    Donc je dirais que oui, nous recommandons toujours la possibilité d’utiliser Angular + Cordova si vous avez déjà les connaissances du framework Angular et que vous souhaitez mettre rapidement en ligne une application de qualité grâce à vos compétences.

    Vous pouvez aussi utiliser la variante Ionic si votre design n’est pas trop particulier et profiter ainsi des composants déjà faits. Enfin, si vous connaissez React et que vous souhaitez miser sur d’avantage de performances (parce que votre application est gourmande), vous pouvez utiliser React Native.

    Par ailleurs, nous avions fait un panorama complet sur les solutions hybrides qui devrait vous intéresser.
    Vous pouvez le trouver ici : http://fr.slideshare.net/3k1n0/panorama-des-solutions-mobile-hybrides

    En espérant avoir répondu à vos attentes 😉

  4. Jean

    Chouette, un grand merci pour cette réponse construite !

Laisser un commentaire