Découverte d'un composant Flutter

Publié le 20 février 2020 par Victor Darras | framework - flutter - dart

Cet article est publié sous licence CC BY-NC-SA

Bonjour à tous, aujourd’hui pour changer nous allons parler un peu de développement mobile. Récemment chez Synbioz 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.

72189862752bed55ac999ccd57a3f42c

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.

L’équipe Synbioz.
Libres d’être ensemble.