Les Widgets Essentiels de Flutter

Dans Flutter, lorsqu'un widget subit un changement d'état, il reconstruit sa description de l'interface utilisateur. Le framework compare ensuite cette nouvelle description à la précédente pour déterminer les modifications minimales nécessaires à appliquer à l'arbre de rendu sous-jacent, puis actualise l'affichage en conséquence.

Il est crucial de comprendre que les éléments qui représentent réellement l'interface à l'écran dans Flutter sont les Element. Un Widget n'est qu'une description des données de configuration pour un Element. Il est important de noter qu'un seul Widget peut correspondre à plusieurs Element.

StatelessWidget

Un StatelessWidget est un composant qui n'a pas d'état interne mutable. Pour en créer un, il suffit d'étendre la classe de base StatelessWidget et d'implémenter la méthode build(). Puisqu'il est dépourvu d'état, un StatelessWidget ne se rend qu'une seule fois lors de son initialisation. Il ne peut pas se mettre à jour ou se redessiner suite à des interactions utilisateur ou des changements temporels.

import 'package:flutter/material.dart';

class MaSimpleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // La méthode build prend un BuildContext, qui fournit des informations
    // sur la position du widget dans l'arbre des widgets.
    return const MaterialApp(
      home: Center(
        child: Text('Bonjour, Widget Sans État !'),
      ),
    );
  }
}

La méthode build() prend un paramètre BuildContext, qui est une référence au positionnement de ce widget dans l'arbre des widgets. Il permet d'accéder aux informations de l'arbre, comme le thème ou les parents, et de naviguer ou de rechercher des widgets ascendants.

StatefulWidget

Un StatefulWidget est un composant doté d'un état interne dynamique qu'il gère via un objet State associé. Ses variables membres n'ont pas besoin d'être déclarées final. Pour mettre à jour l'interface utilisateur suite à une modification de l'état interne, la méthode setState() doit être appelée. Lorsque Flutter détecte un changement d'état, il déclenche un redessin du widget. Pour créer un StatefulWidget, vous devez hériter de la classe StatefulWidget et surcharger la méthode createState().

import 'package:flutter/material.dart';

class CompteurApp extends StatefulWidget {
  @override
  _CompteurAppState createState() => _CompteurAppState();
}

class _CompteurAppState extends State<CompteurApp> {
  int _valeurCompteur = 0;

  void _incrementerCompteur() {
    setState(() {
      // Mettre à jour l'état et déclencher un redessin du widget
      _valeurCompteur++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Compteur Dynamique')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text('Vous avez appuyé sur le bouton tant de fois :'),
              Text(
                '$_valeurCompteur',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _incrementerCompteur,
          tooltip: 'Incrémenter',
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

Contrairement au StatelessWidget, le StatefulWidget est associé à un état qui peut changer. Toute modification des variables internes doit être encapsulée dans un appel à setState() pour notifier à Flutter que l'état a changé et qu'un redessin est nécessaire. La classe State<T> (où T est le type du StatefulWidget associé) est la classe qui détient cet état mutable.

MaterialApp

Le MaterialApp est un widget essentiel qui enveloppe l'ensemble de votre application Flutter. Il fournit une configuration de base pour une application Material Design, y compris les thèmes, la navigation et les locales.

Propriété Type Description
title String Titre affiché dans le sélecteur d'applications Android. Sans effet sur iOS.
home Widget Le widget affiché par défaut au démarrage de l'application.
routes Map<String, WidgetBuilder> Une table des routes de haut niveau pour la navigation.
theme ThemeData Définit le thème visuel global de l'application (couleurs, polices, etc.).
theme.primarySwatch MaterialColor Une palette de couleurs Material Design à utiliser comme couleur principale.
theme.primaryColor Color La couleur principale unique pour la barre d'applications et autres éléments.

AppBar

L'AppBar est un widget qui représente la barre d'applications Material Design, généralement affichée en haut d'un Scaffold. Elle peut contenir un titre, des boutons d'action et d'autres éléments de l'interface utilisateur.

Propriété Nom Description
leading Widget Un widget affiché avant le titre, généralement un bouton de navigation (ex: retour).
title Widget Le titre principal affiché dans la barre d'applications.
actions List<Widget> Une liste de widgets affichés après le titre, souvent des icônes d'action.
flexibleSpace Widget Un widget placé derrière la barre d'applications et son bottom, pouvant s'étirer.
bottom PreferredSizeWidget Un widget personnalisé affiché en bas de l'AppBar, souvent un TabBar.
elevation double Contrôle l'élévation et l'ombre sous l'AppBar.
backgroundColor Color La couleur de fond de la barre d'applications.
brightness Brightness Luminosité du matériel de la barre d'applications (obsolète, utilisez systemOverlayStyle).
textTheme TextTheme Paramètres du thème de texte pour les widgets enfants de l'AppBar.
primary bool Indique si l'AppBar doit être affichée en haut de la barre des tâches (généralement true).
centerTitle bool Indique si le titre doit être centré (par défaut false sur Android, true sur iOS).
titleSpacing double L'espacement horizontal entre le widget leading et le titre.
toolbarOpacity double L'opacité de la barre d'outils, de 1 (opaque) à 0 (transparent).

Scaffold

Le Scaffold est un widget qui implémente la structure de base d'une application Material Design, comme une AppBar, un Drawer (tiroir de navigation), un SnackBar, et un FloatingActionButton. Il occupe l'intégralité de l'écran disponible.

Propriété Type Description
appBar AppBar La barre d'applications à afficher en haut de l'écran.
body Widget Le contenu principal du Scaffold, occupant l'espace restant sous l'AppBar.
bottomNavigationBar BottomNavigationBar La barre de navigation inférieure, affichée en bas de l'écran.
drawer Drawer Un panneau coulissant de navigation (tiroir) affiché depuis le côté gauche.
floatingActionButton FloatingActionButton Un bouton d'action flottant, généralement placé en bas à droite.

Voici un exemple d'utilisation du Scaffold avec une barre de navigation inférieure et un tiroir :

import 'package:flutter/material.dart';

void main() => runApp(ApplicationPrincipes());

class ApplicationPrincipes extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Démonstration Scaffold',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Ma Page d\'Accueil'),
          backgroundColor: Colors.blueAccent,
        ),
        body: const Center(
          child: Text(
            'Contenu de la page principale',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
        ),
        bottomNavigationBar: BottomNavigationBar(
          type: BottomNavigationBarType.fixed, // Ou .shifting
          currentIndex: 0,
          items: const [
            BottomNavigationBarItem(
                icon: Icon(Icons.home),
                label: Text('Accueil')
            ),
            BottomNavigationBarItem(
                icon: Icon(Icons.explore),
                label: Text('Explorer')
            ),
            BottomNavigationBarItem(
                icon: Icon(Icons.settings),
                label: Text('Paramètres')
            )
          ],
          onTap: (index) {
            // Logique de navigation ou de changement d'état ici
            print('Élément de la barre inférieure tapé : $index');
          },
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            print('Bouton flottant pressé !');
          },
          child: const Icon(Icons.add),
        ),
        drawer: Drawer(
          child: ListView(
            padding: EdgeInsets.zero,
            children: const <Widget>[
              DrawerHeader(
                decoration: BoxDecoration(
                  color: Colors.blueAccent,
                ),
                child: Text(
                  'Menu de Navigation',
                  style: TextStyle(color: Colors.white, fontSize: 24),
                ),
              ),
              ListTile(
                leading: Icon(Icons.message),
                title: Text('Messages'),
              ),
              ListTile(
                leading: Icon(Icons.account_circle),
                title: Text('Profil'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Widgets Texte

Le widget Text est utilisé pour afficher une chaîne de caractères sur l'écran. Il offre de nombreuses options de style et de mise en page.

Propriété Type Description
data String Le contenu textuel à afficher.
style TextStyle Le style visuel à appliquer au texte (police, couleur, taille, etc.).
strutStyle StrutStyle Définit les propriétés de la 'strut' (hauteur minimale de ligne).
textAlign TextAlign L'alignement horizontal du texte dans son espace disponible.
textDirection TextDirection La direction de lecture du texte (gauche-à-droite (LTR) ou droite-à-gauche (RTL)).
locale Locale Identifiant pour la sélection de la langue et du formatage utilisateur.
softWrap bool Indique si le texte doit s'enrouler sur plusieurs lignes si nécessaire.
overflow TextOverflow Comment gérer le texte qui dépasse de son conteneur (troncature, fondu, etc.).
textScaleFactor double Facteur d'échelle pour la taille du texte par rapport à la taille par défaut.
maxLines int Le nombre maximum de lignes à afficher. Un dépassement entraînera un overflow.
semanticsLabel String Une étiquette sémantique pour l'accessibilité, lue par les lecteurs d'écran.

Widgets Boutons

La bibliothèque de composants Material offre plusieurs widgets de boutons. Il est important de noter que RaisedButton, FlatButton et OutlineButton sont obsolètes et remplacés par des widgets plus modernes comme ElevatedButton, TextButton et OutlinedButton respectivement. FloatingActionButton et IconButton restent valides et largement utilisés.

  • ElevatedButton (remplace RaisedButton)
  • TextButton (remplace FlatButton)
  • OutlinedButton (remplace OutlineButton)
  • FloatingActionButton
  • IconButton

Voici un exemple montrant l'utilisation de différents types de boutons :

import 'package:flutter/material.dart';

void main() => runApp(DemoBoutons());

class DemoBoutons extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Exemple de Boutons Flutter',
      theme: ThemeData(primarySwatch: Colors.indigo),
      home: Scaffold(
        appBar: AppBar(title: const Text('Widgets Boutons')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              ElevatedButton(
                onPressed: () => print('Bouton Élevé pressé !'),
                child: const Text('Bouton Élevé'),
              ),
              TextButton(
                onPressed: () => print('Bouton Texte pressé !'),
                child: const Text('Bouton Texte'),
              ),
              OutlinedButton(
                onPressed: () => print('Bouton Contour pressé !'),
                child: const Text('Bouton Contour'),
              ),
              FloatingActionButton.extended(
                onPressed: () => print('Bouton Flottant Étendu pressé !'),
                label: const Text('Créer Nouveau'),
                icon: const Icon(Icons.add),
              ),
              IconButton(
                onPressed: () => print('Bouton Icône pressé !'),
                icon: const Icon(Icons.favorite),
                color: Colors.red,
                iconSize: 30.0,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Propriétés communes aux boutons (certaines sont désormais gérées par ButtonStyle pour les nouveaux boutons) :

Propriété Type Description
textColor Color Couleur du texte du bouton (souvent définie via ButtonStyle.foregroundColor).
disabledTextColor Color Couleur du texte lorsque le bouton est désactivé.
color Color Couleur de fond du bouton (souvent définie via ButtonStyle.backgroundColor).
highlightColor Color Couleur de surbrillance lorsque le bouton est pressé.
splashColor Color Couleur de l'effet d'ondulation lors d'un clic.
colorBrightness Brightness Luminosité du thème du bouton (déprécié).
padding EdgeInsetsGeometry Espacement interne du bouton.
shape ShapeBorder Forme du bouton (arrondi, rectangulaire, etc.).
child Widget Le widget enfant affiché à l'intérieur du bouton.
onPressed VoidCallback La fonction de rappel à exécuter lorsque le bouton est pressé. Si null, le bouton est désactivé.

Widgets Image

Le widget Image est utilisé pour charger et afficher des images de différentes sources. Flutter offre plusieurs constructeurs pour différentes méthodes de chargement :

  • Image : Le constructeur de base pour charger une image via un ImageProvider.
  • Image.asset : Pour charger une image depuis les assets locaux de l'appplication.
  • Image.file : Pour charger une image depuis un fichier sur le système de fichiers.
  • Image.network : Pour charger une image depuis une URL réseau.
  • Image.memory : Pour charger une image depuis un bloc de données en mémoire (Uint8List).
Propriété Type Description
scale double Le facteur d'échelle pour l'image.
semanticsLabel String Une étiquette sémantique pour l'accessibilité, décrivant le contenu de l'image.
width double La largeur de l'image. Si null, affiche la largeur naturelle.
height double La hauteur de l'image. Si null, affiche la hauteur naturelle.
color Color Une couleur à mélanger avec l'image (nécessite colorBlendMode défini).
fit BoxFit Le mode d'ajustement de l'image dans son conteneur.
alignment Alignment L'alignement de l'image si elle ne remplit pas son conteneur.
repeat ImageRepeat Comment répéter l'image si elle est plus petite que l'espace disponible.
centerSlice Rect Une découpe centrale pour les images de type '9-patch'.
matchTextDirection bool Si true, l'image est inversée pour les directions de texte RTL.
gaplessPlayback bool Si true, maintient l'ancienne image pendant le chargement d'une nouvelle.
filterQuality FilterQuality La qualité de filtrage de l'image lors de la mise à l'échelle.

Les modes de remplissage (BoxFit) :

  • BoxFit.fill : L'image remplit complètement l'espace, pouvant être déformée.
  • BoxFit.contain : L'image est entièrement contenue dans l'espace, en respectant son rapport d'aspect. Peut laisser des espaces vides (letterboxing).
  • BoxFit.cover : L'image couvre l'espace en coupant les bords si nécessaire, sans déformation.
  • BoxFit.fitWidth : L'image remplit la largeur et s'ajuste proportionnellement en hauteur, coupant si elle dépasse.
  • BoxFit.fitHeight : L'image remplit la hauteur et s'ajuste proportionnellement en largeur, coupant si elle dépasse.
  • BoxFit.scaleDown : Similaire à contain, mais réduit l'image si elle est trop grande pour tenir, sans l'agrandir.
  • BoxFit.none : L'image est affichée à sa taille originale, sans ajustement ni coupure, et n'est pas mise à l'échelle.

Flutter met en cache les images par défaut. La taille maximale du cache d'images est de 1000 images et 100 Mo.

Widgets Icônes

Les widgets d'icônes affichent des symboles graphiques. Flutter propose des icônes Material Design intégrées et la possibilité d'utiliser des images comme icônes.

  • IconButton : Un widget icône interactif, réagit aux pressions.
  • Icons : Un ensemble prédéfini d'icônes Material Design.
  • IconTheme : Définit le thème par défaut (couleur, taille) pour les icônes enfants.
  • ImageIcon : Affiche une icône à partir d'une image (par exemple, un asset).
Propriété Type Description
size double La taille de l'icône en pixels logiques.
color Color La couleur de l'icône.
semanticsLabel String Une étiquette sémantique pour l'accessibilité.
textDirection TextDirection La direction de dessin de l'icône (affecte les icônes directionnelles).

Widgets d'Entrée de Texte (TextField)

Le widget TextField est un champ de texte interactif permettant à l'utilisateur de saisir du texte. Il offre de nombreuses options de personnalisation et de gestion de l'entrée.

Propriété Description
controller Contrôleur pour accéder et modifier le texte du champ, ainsi que pour écouter les changements.
focusNode Contrôle si le TextField a le focus d'entrée, gérant l'interaction clavier.
decoration Définit l'apparence visuelle du TextField (hint, label, icône, bordure, etc.) via un InputDecoration.
textAlign Alignement horizontal du texte à l'intérieur du champ.
textDirection Direction du texte à l'intérieur du champ.
keyboardType Définit le type de clavier à afficher (numérique, email, texte, etc.).
textInputAction Définit l'action du bouton 'Entrée' sur le clavier (ex: 'Done', 'Next').
style Le style du texte saisi dans le champ.
autofocus Si true, le champ obtient automatiquement le focus au démarrage (par défaut false).
obscureText Si true, le texte saisi est masqué (utile pour les mots de passe).
maxLines Le nombre maximum de lignes du champ de texte. Par défaut 1. Si null, supporte plusieurs lignes.
maxLength Le nombre maximum de caractères autorisés. Affiche un compteur de caractères si défini.
onChanged Fonction de rappel appelée chaque fois que le contenu du texte change.
onEditingComplete Fonction de rappel appelée lorsque l'édition est terminée (ex: envoi du formulaire via le clavier).
onSubmitted Fonction de rappel appelée lorsque l'utilisateur soumet le texte (retourne le contenu).
inputFormatters Une liste de formateurs pour contraindre l'entrée du texte (ex: chiffres uniquement).
enabled Si false, le champ de texte est désactivé et ne peut pas être modifié.
enableInteractiveSelection Si true, permet la sélection de texte interactive (copier, couper, coller).
keyboardAppearance Le mode de luminosité du clavier (spécifique à iOS).
onTap Fonction de rappel appelée lorsque le TextField est tapé.
buildCounter Callback pour personnaliser l'affichage du compteur de caractères (via InputDecorator.counter).

Widgets Formulaire (Form)

Le widget Form est un conteneur pour des champs de saisie de formulaire. Il permet de regrouper et de valider plusieurs FormField (comme TextFormField) simultanément.

Propriété Type Description
child Widget Le widget enfant du formulaire, contenant les champs.
autovalidateMode AutovalidateMode Mode de validation automatique du formulaire (ex: onUserInteraction pour valider à chaque saisie).
onWillPop WillPopCallback Fonction de rappel appelée lorsque l'utilisateur tente de quitter la route du formulaire.
onChanged VoidCallback Fonction de rappel appelée lorsque l'un des éléments du formulaire change.

Voici un exemple de formulaire avec des champs de validation :

import 'package:flutter/material.dart';

void main() {
  runApp(DemoFormulaire());
}

class DemoFormulaire extends StatefulWidget {
  @override
  _DemoFormulaireState createState() => _DemoFormulaireState();
}

class _DemoFormulaireState extends State<DemoFormulaire> {
  final GlobalKey<FormState> _cleFormulaire = GlobalKey<FormState>();
  String? _nomUtilisateurSaisi;
  String? _motDePasseSaisi;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Formulaire Flutter",
      theme: ThemeData(
        primarySwatch: Colors.deepPurple,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text("Composant Formulaire"),
        ),
        body: Form(
          key: _cleFormulaire,
          autovalidateMode: AutovalidateMode.onUserInteraction, // Validation en temps réel
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                TextFormField(
                  decoration: const InputDecoration(
                    labelText: "Nom d'utilisateur",
                    hintText: "Entrez votre nom d'utilisateur",
                    prefixIcon: Icon(Icons.person),
                    border: OutlineInputBorder(),
                  ),
                  validator: (valeur) {
                    if (valeur == null || valeur.isEmpty) {
                      return "Le nom d'utilisateur est requis.";
                    }
                    if (valeur.length < 6) {
                      return "Le nom d'utilisateur doit avoir au moins 6 caractères.";
                    }
                    return null; // Retourne null si la validation réussit
                  },
                  onSaved: (valeur) {
                    _nomUtilisateurSaisi = valeur;
                  },
                ),
                const SizedBox(height: 20),
                TextFormField(
                  decoration: const InputDecoration(
                    labelText: "Mot de passe",
                    hintText: "Choisissez un mot de passe fort",
                    prefixIcon: Icon(Icons.lock),
                    border: OutlineInputBorder(),
                  ),
                  obscureText: true,
                  validator: (valeur) {
                    if (valeur == null || valeur.isEmpty) {
                      return "Le mot de passe est requis.";
                    }
                    if (valeur.length < 8) {
                      return "Le mot de passe doit avoir au moins 8 caractères.";
                    }
                    return null; // Retourne null si la validation réussit
                  },
                  onSaved: (valeur) {
                    _motDePasseSaisi = valeur;
                  },
                ),
                const SizedBox(height: 30),
                ElevatedButton(
                  onPressed: () {
                    if (_cleFormulaire.currentState?.validate() == true) {
                      _cleFormulaire.currentState?.save(); // Sauvegarde les valeurs des champs
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('Connecté en tant que: ${_nomUtilisateurSaisi ?? ''}')),
                      );
                      print("Nom d'utilisateur: $_nomUtilisateurSaisi");
                      print("Mot de passe: $_motDePasseSaisi");
                    } else {
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('Veuillez corriger les erreurs du formulaire.')),
                      );
                    }
                  },
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(vertical: 15),
                    backgroundColor: Theme.of(context).primaryColor,
                    foregroundColor: Colors.white,
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                  child: const Text("Connexion"),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Widgets Conteneur (Container)

Le Container est un widget polyvalent qui peut contenir un seul enfant et offrir diverses options de style, de positionnement et de dimensionnement, telles que l'alignement, les marges (margin), les rembourrages (padding) et la décoration.

Propriété Type Description
key Key Identifiant unique du widget pour la recherche et la mise à jour.
alignment AlignmentGeometry Alignement du widget enfant à l'intérieur du conteneur.
padding EdgeInsetsGeometry Marge interne entre le bord du conteneur et son enfant.
color Color La couleur de fond du conteneur.
decoration Decoration Décoration de fond (bordures, ombres, dégradés, etc.).
foregroundDecoration Decoration Décoration de premier plan (dessinée par-dessus l'enfant).
width double La largeur spécifique du conteneur.
height double La hauteur spécifique du conteneur.
constraints BoxConstraints Les contraintes de taille imposées au conteneur.
margin EdgeInsetsGeometry Marge externe autour du conteneur.
transform Matrix4 Une matrice de transformation 4x4 à appliquer au conteneur.
child Widget Le widget enfant contenu dans le Container.

Le padding représente l'espace interne entre le contenu du conteneur et ses propres limites, tandis que le margin est l'espace externe qui sépare le conteneur des autres widgets. Les interactions tactiles sont reconnues dans la zone de padding, mais pas dans celle de margin.

Modèle de Contraintes de Boîte

Le modèle de contraintes de boîte en Flutter permet aux widgets de définir leur propre taille et positionnement en fonction de limites spécifiées par leurs parents. Les widgets courants qui exploitent ce modèle sont ConstrainedBox, SizedBox et UnconstrainedBox.

ConstrainedBox

ConstrainedBox est un widget qui applique des contraintes supplémentaires à son enfant. Il prend un paramètre BoxConstraints qui spécifie les contraintes de largeur et de hauteur minimales et maximales.

// Constructeur principal
const ConstrainedBox({
  Key? key,
  required BoxConstraints constraints, // Les contraintes à appliquer
  Widget? child,
})

SizedBox

SizedBox est utilisé pour donner à son enfant une taille fixe (largeur et/ou hauteur). C'est une façon concise d'appliquer des contraintes de taille spécifique.

// Constructeur principal
const SizedBox({
  Key? key,
  double? width,  // Largeur fixe
  double? height, // Hauteur fixe
  Widget? child,
})

Le widget SizedBox est une forme spécialisée de ConstrainedBox.

UnconstrainedBox

UnconstrainedBox est un widget qui supprime toutes les contraintes de son parent pour permettre à son enfant de se dimensionner librement selon sa taille intrinsèque. Il est généralement utilisé dans des cas spécifiques où un enfant doit ignorer les contraintes parentales (par exemple, pour provoquer un dépassement visuel intentionnel).

Étiquettes: Flutter Widgets StatelessWidget StatefulWidget MaterialApp

Publié le 17 juillet à 09h51