Article écrit par Victor Darras
Aujourd’hui, nous allons parler un peu de développement mobile. Récemment chez Ouidou nous avons pris du temps pour essayer un nouvel environnement de travail composé de Dart, un langage statiquement typé, orienté objet avec une syntaxe relativement proche de JavaScript ; et Flutter, un framework complet dédié à la création d’apps mobiles multi-plateformes. C’est-à-dire qu’une seule codebase doit pouvoir être compilée pour toutes les plateformes (en l’occurrence, iOS, Android, Desktop et Web).
Dans un article dédié à leur découverte, je vais aborder l’écriture d’un composant relativement simple mais entièrement personnalisé. Il existe avec Flutter un ensemble de composants préétablis dans des bibliothèques comme Material ou Cupertino (respectivement prévues à destination d’Android ou iOS). Mais je voudrais afficher un élément de formulaire qui n’appartienne à aucun de ces 2 standards.
Je tiens à préciser (mais j’imagine que ça sera clair par la suite) que je ne suis pas encore à l’aise avec toutes les notions de Flutter, et quelques erreurs peuvent se glisser dans cet article. Nous nous concentrerons surtout sur l’affichage, parce que c’est ce qui me parle le plus en tant qu’intégrateur.
Quelques précisions avant de démarrer
Nous voulons donc afficher un bloc à fond blanc sur toute la largeur de l’écran, contenant un label sur la gauche, suivi d’une valeur à droite qui pourra être éditée en appuyant sur tout le bloc. L’ensemble est souligné d’un discret trait gris qui délimitera visuellement plusieurs éléments de formulaire. Nous l’appellerons CustomFormInput
.
Dans un formulaire que je ne détaillerai pas ici, nous voudrions pouvoir appeler un simple composant en lui passant les paramètres comme suit :
// Enfant d'un composant Column par exemple CustomFormInput( label: "Email", // Simple chaîne de caractères value: email, ),
Importer un nouveau composant
De la même manière qu’une app Flutter utilise un package material
avec une bibliothèque de composants par défaut (aux airs de Material Design) nous allons charger un nouveau fichier :
import 'package:flutter/material.dart'; import 'package:app_name/common/custom_form_input.dart'; // app_name fait référence à la racine de votre codebase
Et nous créons dans la foulée un fichier custom_form_input.dart
dans un dossier common
(par exemple) qui nous permettra de centraliser d’autres composants du même acabit.
Écrire un nouveau composant
Comme dans le fichier précédent nous allons charger quelques composants par défaut dont nous pourrons hériter par la suite. Nous aurons besoin pour ce composant d’un « état » qui permet de récupérer des données d’un parent et de lui faire part des changements éventuels. Nous allons faire appel à la classe StatefulWidget et en hériter pour créer notre CustomFormInput
:
import 'package:flutter/material.dart'; class CustomFormInput extends StatefulWidget { final String label; final String value; CustomFormInput({Key key, this.label, this.value}) : super(key: key); @override _CustomFormInputState createState() => _CustomFormInputState(); }
Comme précisé plus haut nous avons donc 2 propriétés label
et value
. Nous appelons ensuite notre constructeur tout juste déclaré, ses arguments étant liés à la classe courante avec this
. Avec super
nous écrasons l’attribut key
de la classe parente. Celui-ci permet à FLutter de détecter quelle instance du widget a été modifiée dans une liste (à la manière de l’attribut key
dans une liste en VueJS).
À la suite de la méthode @override
(qui nous prévient au build si aucune méthode n’est écrasée) nous pouvons ensuite initialiser un state avec la méthode createState()
que nous appellerons _CustomFormInputState
.
État, done ; passons aux choses sérieuses
Je ne m’attarderai pas sur cette partie mais dans les grandes lignes nous définissons nos variables. On les initialise dans initState
avec notamment notre contrôleur TextEditingController
. Le dispose
permet de nous assurer de libérer toutes les ressources qui auraient pu être utilisées par le contrôleur.
class _CustomFormInputState extends State<CustomFormInput> { String _value ; TextEditingController _controller; void initState() { _value = widget.value; _controller = TextEditingController(); _controller.text = widget.value; super.initState(); } void dispose() { _controller.dispose(); super.dispose(); } @override // here we'll override the class build method
Affichage du composant
Nous allons englober notre composant dans une classe GestureDetector
qui nous permet de définir une action au clic/tap avec onTap
que nous appellerons _displayDialog
et prend un seul widget comme enfant.
Pour obtenir la bordure en bas de notre bloc nous allons commencer par un widget DecoratedBox
. Sa propriété decoration
peut ainsi prendre comme valeur un widget BoxDecoration
avec pour argument à la propriété border
un widget Border
. La méthode bottom
de ce dernier prend quant à elle une valeur définie par le widget BorderSide
dont les propriétés width
et color
auront respectivement les valeurs 1
et Color(0xFFD1D3DB)
(soit la couleur hexadécimale #D1D3DB
).
/* Petit parallèle pour intégrateurs web */ .CustomFormInput { border-bottom: 1px solid #D1D3DB; }
Si vous avez compris le principe je vais me permettre d’accélérer un peu, nous avons ensuite un widget Padding
, qui prend une valeur de padding et un enfant. Dans le widget Row
, qui nous permet de placer nos éléments horizontalement, nous indiquons que les widgets enfants doivent être séparés les uns des autres avec MainAxisAlignment.spaceBetween
(à la manière d’un align-items: space-bewteen
en CSS).
Les enfants qui suivent n’ont pas de comportement à proprement parler, ils se soucient seulement d’afficher du texte avec une couleur customisée pour le label.
Widget build(BuildContext context) => GestureDetector( onTap: () => _displayDialog(context), child: DecoratedBox( decoration: BoxDecoration( border: Border(bottom: BorderSide(width: 1, color: Color(0xFFD1D3DB))) ), child: Padding( padding: EdgeInsets.all(15.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( widget.label, style: TextStyle(color: Color(0xFFA3A7B7), fontSize: 16) ), Text( _value, style: TextStyle(fontSize: 16) )])),), ), // GestudeDetector end
À la suite de cette méthode de build, nous allons déclarer la méthode _displayDialog
que nous avions appelée dans la méthode onTap
précédemment.
C’est assez simple, un widget showDialog
permet de gérer la transition et le « fond » ou overlay de la modale qui viendra s’afficher au-dessus du contenu existant. Ce widget prend un context
en premier argument, lui permettant de recevoir ou passer des données aux widgets qui le contiennent puis un widget Dialog
en second argument.
Dans notre cas nous utilisons un AlertDialog
qui permet d’afficher un contenu basique et quelques boutons. Ici nous aurons besoin d’un titre rappelant le champ sélectionné et d’un TextField
pour éditer notre valeur email
. Le bouton de validation se trouve dans l’attribut actions
auquel nous passons un FlatButton
qui, une fois pressé, modifie le champ et ferme le widget showDialog
.
_displayDialog(BuildContext context) async { return showDialog( context: context, builder: (context) { return AlertDialog( title: Text(widget.label), content: TextField( controller: _controller, ), actions: <Widget>[ FlatButton( child: Text('OK'), onPressed: () { setState((){ _value = _controller.text; }); Navigator.of(context).pop(); },)],); }); }
En conclusion
Nous avons donc vu dans les grandes lignes comment créer et importer un composant, comment le faire réagir à une action de l’utilisateur, mais surtout en customiser l’affichage. J’espère que cet article vous aura donné envie d’essayer Flutter et vous aura fait découvrir quelques-uns de ses composants les plus utilisés. Si vous trouvez des non-sens ou autres inexactitudes, n’hésitez pas à l’indiquer dans les commentaires, tout est bon à prendre.