13. Exercice : ToDo App

La todoapp

Après avoir vu les bases de Dart et analysé l’application de démo, il est l’heure de mettre les mains dans le cambouis et de commencer à faire notre propre application. Durant les divers cours de Flotteur, il y aura plusieurs applications de faites sur lesquelles nous reviendrons régulièrement afin de les faire évoluer tel le plus beau des Pokémon.

La première version de cette application permettra de créer une tâche, la cochée/décochée et supprimer toutes les tâches finies. Lors d’un autre cours, les tâches seront sauvegardées sur une base de donnée locale.

Pour rappel, vous pouvez très bien faire cette application sur https://dartpad.dev/sans avoir besoin d’installer tout l’environnement Flutter sur votre ordinateur.

Pour commencer, vous allez créer un nouveau projet sur votre IDE, les étapes sont indiquées dans l’analyse de l’application démo, Counter.

Vous avez donc cette Counter App, vous allez supprimer tout le contenu de la page (soit main.dart) et nous allons reprendre de zéro.

Comme nous l’avons déjà vu, le point d’entrée d’une application est la fonction main. Vous allez donc écrire :

Cette fonction, même avec un émulateur de lancé, ne produira rien. Pour cela il faut indiquer qu’on cherche à lancer une application. Il faudra donc ajouter :

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

TodoApp sera surligné, car cette classe n’existe pas encore. Nous allons alors la créer.

Là encore, ToDoApp sera surligné, car runApp s’attend à une classe de type Widget. Il nous faut donc ajouter :

class TodoApp extends StatelessWidget {}

Rappelez-vous que tout est widget avec Flutter de ce fait, il est normal que l’application soit elle-même un enfant de Widget. Cette fois-ci, c’est la classe qui est surlignée comme étant une erreur. En effet, un StatelessWidget doit forcément contenir la méthode build().

class TodoApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
 return Container();
 }
}

Ici, on retourne pour le moment un container, car build doit forcément retourner un widget puisque c’est cette méthode qui permet d’afficher du contenu à l’écran.

Il nous faut indiquer que l’on souhaite retourner un widget de type MaterialApp, qui nous permettra de définir un thème, des routes, et beaucoup d’autres paramètres, mais surtout, quel widget sera le premier à s’afficher sur l’application. La homepage de notre application sera le widget ToDoHome.

class TodoApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
 return MaterialApp(
 home: ToDoHome(),
 );
 }
}

Comme pour ToDoApp, il nous faudra créer un widget, cependant, celui-ci sera de type Statefull, soit le même type de widget que pour la Counter App. Pour rappel, un StatefullWidget peut se reconstruire afin de se mettre à jour avec de nouvelles données grâce à une fonction qui lui est particulière, setState().

class ToDoHome extends StatefulWidget {
 @override
 State<ToDoHome> createState() => _ToDoHomeState();
}

class _ToDoHomeState extends State<ToDoHome> {
 @override
 Widget build(BuildContext context) {
 return Scaffold();
 }
}

Nous avons donc enfin la base pour mettre en place les fonctionnalités de notre application. Premièrement, il nous faut créer un objet ToDo, qui comportera un nom et un état (actif ou non).

Afficher une liste de tâches

class ToDo {
 final String name;
 bool isDone;

 ToDo({required this.name, this.isDone = false});
}

Name ne changera jamais de valeur, du coup, il est final et est indiqué comme requis dans le constructeur, tandis que isDone est modifiable et par défaut sera à false, pour indiquer que la tâche sera non cochée à sa création.

Créon une liste de ToDo avant la méthode build. Si vous mettez cette liste dans la méthode build, elle sera recréée à chaque rafraîchissement de la vue, ce qui casserait totalement la fonctionnalité voulue.

Il est temps maintenant de jeter un œil à ListView, qui vous permet, attention, vous n’êtes pas près, d’afficher des listes ! Voici à quoi ressemble une ListView dans notre projet.

ListView.builder(
 itemCount: todoList.length,
 itemBuilder: (context, index) {
 var todo = todoList[index];
 return Text(todo.name);
 }),

Il y a plusieurs façons d’instancier une ListView, ici on va utiliser la méthode builder qui permet de créer des éléments à la demande et ne fait le rendu que des éléments actuellement visibles. C’est très efficace quand vous avez une liste potentiellement infinie. Si vous avez 1000 éléments, le builder ne fait le rendu que de ceux que vous avez à l’écran. Vous devez donc, en échange, donner une taille de liste et un widget à cette fonction pour qu’elle fasse son travail.

Voilà comment la liste s’intègre dans notre projet :

return Scaffold(
 body: ListView.builder(
 itemCount: todoList.length,
 itemBuilder: (context, index) {
 var todo = todoList[index];
 return Text(todo.name);
 }),
 );

Pour le moment, on ne retourne que le nom de la todo. Afin d’avoir une idée du rendu, je vous propose d’ajouter manuellement des objets dans la liste grâce à la méthode initState. Cette méthode est spécifique aux widgets ayant un état comme le StatefullWidget. Elle est appelée une seule fois lors de la création du widget. Peu importe le nombre de fois que vous rafraîchirez la vue, elle ne sera jamais rappelée. Si vous effectuez des modifications dans cette méthode, il vous faudra, dans notre cas, recompiler l’application.

@override
 void initState() {
 super.initState();
 todoList.add(ToDo(name: 'Acheter du café'));
 todoList.add(ToDo(name: 'Courrir'));
 todoList.add(ToDo(name: 'Entrainement speedrun'));
 }

Recompilez l’application, vous devriez avoir ce rendu.

Pour le moment il n’y pas d’élément visuel pour indiquer qu’une tâche est faite ou pas, et on peut voir que les textes sont tous collés en haut à gauche. On mettra un petit coup de peinture dessus rapidement.

Pour le moment, il nous faut changer le widget Text vers un widget plus complexe, le CheckboxListTile. Le CheckboxListTile va nous permettre de combiner un texte, une checkbox et le changement d’état de cette dernière. Ajouter ces lignes à votre projet à la place du widget Text :

return CheckboxListTile(
 title: Text(todo.name),
 value: todo.isDone,
 onChanged: (bool? value) {
 
 },
 );

Le title demande un widget, vous allez y mettre un text mais vous pourriez y mettre une image, une liste, ou autre chose, ce qui rend le widget assez personnalisable.La value permet d’indiquer à la partie checkbox du widget si elle est cochée ou non, ici on reprendra donc l’attribut isDone de l’objet todo.

Pour finir, onChanged() est un callback qui sera appelé à chaque tap/click sur la CheckboxListTile, il retourne un booléen qui indiquera ne nouvel état de la checkbox.

Vous avez peut-être remarqué que la valeur du callback est nullable avec sa notation bool?. Cela est du au fait qu’il est possible d’avoir 3 états de checkbox, true, false et null si l’attribue tristate de la CheckboxListTile est activée.

Dans notre cas, nous n’auront pas à gérer ce cas car cette option ne sera pas activée, cependant je pense qu’il était important de vous expliquer pourquoi le booléen du callback était nullable.

Dans notre projet, on va devoir donc mettre à jour isDone à chaque tap sur la tuile. Mais pour cela il faudra gérer la partie nullable de la valeur retournée, car dans notre cas, ToDo n’accepte pas de nullable. Il y a plusieurs manières de gérer ce cas, je vais vous en proposer une. Dans le cas où la valeur retournée serait nulle, on indiquera que par défaut la nouvelle valeur de isDone sera false grâce à ??.

onChanged: (bool? value) {
 todo.isDone = value ?? false;
 },

Recompilez votre application et testez de cocher les checkbox….Tiens, ça ne marche pas ? Pourquoi à votre avis ?On sait qu’on se trouve dans un StateFullWidget, il faudra donc que cette vue se rafraichisse afin que le visuel soit cohérent avec le nouvel état de la liste des ToDo.

Pour mettre à jour l’interface visuelle, il nous faut utiliser setState, cette méthode contient un callback dans lequel il nous faudra intégrer uniquement les lignes de codes qui auront un impact sur la vue.

setState(() {
 todo.isDone = value ?? false;
 });

Je m’explique plus en détail, imaginons que l’application soit connectée à un serveur, cette valeur, une fois changée, fera un appel au serveur, pour indiquer la nouvelle valeur de l’élément dans la liste. Cette méthode, qu’on appellerait par exemple updateServerData(), n’aurait pas sa place dans setState, car elle n’a pas d’impact visuel, on peut dire. SetState ne devrait contenir que ce qui modifie l’état de la vue, ce sont en tout cas les recommandations de Flutter.

Vous pouvez à nouveau compiler votre application. Tout devrait être fonctionnel.

Création des tâches

Maintenant qu’on sait afficher des tâches et les activer/désactiver, on va mettre en place la fonctionnalité pour ajouter des tâches avec le nom de notre choix.Pour cela, nous allons nous inspirer de la CounterApp, et ajouter un FloatingActionButton, souvenez-vous, c’est le gros bouton + qui se trouve en bas à droite de l’écran.

Pour ajouter cet élément, il nous faut ajouter un attribut dans le Scaffold.

floatingActionButton: FloatingActionButton(
 child: const Icon(Icons.add),
 onPressed: () {
 addTask();
 },
 ),

Intégrons directement la méthode addTodo dedans et créons-la.

Avec cette method, on va afficher une pop up qui contiendra un champ de texte, ainsi que deux boutons, “Annuler” et “Ajouter”. Pour ce faire, nous allons utiliser la méthode showDialog qui permet d’afficher du contenu au-dessus d’une vue. Cette méthode aura besoin du context, celui retourné par la méthode build(), et d’un builder, qui, comme pour la ListView, sera utilisée pour afficher du contenu.

showDialog(
 context: context,
    builder: (context) {
 });
 }

Ce contenu que nous allons afficher est donc un widget de type Dialog. Il en existe plusieurs, mais nous allons utiliser AlertDialog.Dans ce dernier, nous allons intégrer un titre, du contenu et des actions.Le title, comme pour les elements de la liste, est un widget, ici de type Text, le contenu sera tout le corps de l’alerte et peut-être n’importe quel type de widget, vous pourriez y mettre une liste par exemple.

Les actions sont les boutons en bas d’une alerte, l’un permet de quitter la vue sans effectuer de modification de données, l’autre si.

var controller = TextEditingController();
return AlertDialog(
 title: const Text('Ajouter une tâche'),
 content: TextField(
 autofocus: true,
 controller: controller,
 ),
 actions: [
 TextButton(
 onPressed: () {
 Navigator.of(context).pop();
 },
 child: const Text('Annuler')),
 TextButton(
 onPressed: () {
 setState(() {
 if (controller.value.text.isNotEmpty) {
 todoList.add(ToDo(name: controller.value.text));
 Navigator.of(context).pop();
 }
 });
 },
 child: const Text('Ajouter'))
 ],
 );

Voici une capture afin que vous ayez sous les yeux le rendu de votre code.

Analysons un peu plus ce code. Je ne vais pas non plus juste vous faire faire des copier coller sans vous faire comprendre ce que vous codez !

Dans notre AlertDialog, nous allons utiliser un TextField, c’est un champ de texte ou l’utilisateur va pouvoir taper avec son clavier. Afin d’en récupérer le contenu pour l’ajouter dans la liste, nous allons créer un TextEditingController que nous allons intégrés dans le TextField.

var controller = TextEditingController();
content: TextField(
 autofocus: true,
 controller: controller,
 ),

Ce controller sera ensuite appelé lorsque le bouton “Valider” sera tapé et si son contenu n’est pas vide, une nouvelle tâche sera ajoutée à la liste. Le dernier ligne de cote nous permet de fermer l’AlertDialog, cette fonction vous servira aussi pour quitter une page et revenir à la précédente.

onPressed: () {
 onPressed: () {
    setState(() {
        if (controller.text.isNotEmpty) {
        todoList.add(ToDo(name: controller.text));
        }
    });
    Navigator.of(context).pop();
    },
 },

AppBar

Afin d’embellir un peu cette vue, nous allons rajouter une AppBar, c’est un widget qui se trouve tout en haut d’une vue et qui permet de revenir à la vue précédente, avoir un titre, avoir des actions etc… Voici ce que donne une AppBar dans notre projet :

Pour réaliser ce rendu, il faudra ajouter le code suivant dans le Scaffold :

appBar: AppBar(
 title: Text('ToDo App'),
 ),

Petite note sur le titre dans une AppBar, par défaut, sur Android, il sera ancré à droite, et sur iOS centré, vous pouvez modifier ce comportement avec l’attribut centerTitle.

Maintenant, pour ajouter des actions à une AppBar, soit des boutons, nous allons ajouter le code suivant dans l’objet AppBar :

actions: [
 IconButton(
    onPressed: () {
        setState(() {
            todoList.removeWhere((element) => element.isDone);
        });
    },
    icon: Icon(Icons.delete))
 ],

Lorsque ce bouton sera tappé, chaque tâche qui a été cochée sera supprimer.

Aller plus loin …

Voici quelques pistes si vous voulez continuer à vous entrainer avec ce projet.

  • Trier les tâche pour avoir en premier les tâches pas encore effectuées
  • Changer la couleur des checkbox
  • Afficher un texte quand la liste est encore vide
  • Afficher un compteur de tâches effectuées

Ressources

Flutter fait des vidéos régulièrement pour permettre de faire découvrir des widgets aux développeurs. Le contenu est en anglais, mais vous pouvez ajouter des sous-titres avec la traduction automatique.

CheckboxListTile

ListView