Flutter pour un développeur iOS — Créer une app de blog Partie 2
Avant de commencer
Dans cet article, on va reprendre le développement de notre projet là où il en était resté à la fin de la partie 1, que vous pouvez retrouver ici.
N’hésitez pas non plus à reprendre en cas de besoin le code du projet, que vous pouvez retrouver ici.
Introduction
Pour le moment, notre application est encore vraiment très primaire, puisqu’elle ne présente encore que le header de notre blog. Je vous propose de continuer en intégrant cette fois-ci le contenu du blog lui-même, ainsi que la page des commentaires.
Ce faisant, on abordera comment construire avec Flutter certaines choses très courantes côté iOS (et de manière générale), à savoir :
- Quel est l’équivalent d’une UITableView ?
- Quel est l’équivalent d’une UIScrollView ?
- Comment construire une navigation entre plusieurs écrans ?
- Comment créer des widgets customs ?
En répondant à ces questions, on sera aussi amené à bien travailler la manière d’approcher la composition de l’interface et le layout des différents objets avec Flutter.
L’idée reste toujours d’approcher ces questions en comparant ce qui se fait nativement dans le développement d’applications natives sur iOS, mais, comme dans la partie précédente, il n’est absolument pas nécessaire d’avoir des connaissances dans le domaine pour comprendre l’article en lui-même et suivre le tutoriel.
Sans plus attendre, continuons notre projet.
Nouvelles données
Commençons par ajouter les nouveaux modèles de données dont nous aurons besoin pour coder l’interface de cette partie. En effet, puisque nous allons afficher des commentaires, il va nous falloir procéder à quelques ajouts.
Sommairement, un commentaire est :
- un bloc de texte
- un profil (celui de l’auteur)
Voici donc nos deux nouveaux types de données : Profile et Comment.
Profile
On va simplifier un peu ce que serait un profil dans notre application puisque le but de notre projet est plutôt de regarder comment se construit une interface. Par conséquent, on va composer un profil de deux données : un pseudo, et une photo de profil (qui sera directement une Image pour simplifier).
Créons donc un nouveau fichier profile.dart dans le dossier model, qui contiendra le code suivant :
import 'package:meta/meta.dart';
import 'package:flutter/material.dart';
class Profile {
String username;
Image image;
Profile({@required this.username, @required this.image});
}
Cette classe est construite sur le même modèle que la classe Article. Remarquons à nouveau les ajouts que l’on a fait au constructeur afin d’avoir des paramètres nommés à chaque fois que l’on instance un nouveau profil. Encore une fois, ce n’est en rien obligatoire, mais j’ai tendance à trouver cela plus explicite.
Comment
Pour les mêmes raisons que pour les profils, on va réduire un commentaire à trois données : son contenu textuel, le temps depuis lequel il a été envoyé, et le profil de son auteur.
Dans un nouveau fichier comment.dart, codons la chose suivante :
import 'package:blog_app/model/profile.dart';
import 'package:meta/meta.dart';
class Comment {
String content;
String timeSinceComment;
Profile profile;
Comment(
{@required this.content,
@required this.timeSinceComment,
@required this.profile});
}
Il nous faut donc importer le fichier précédent dans celui-ci pour disposer de la classe Profile. Par rapport à iOS, ce n’est pas tout à fait naturel.
Notez aussi que j’ai réduit le concept de temps depuis publication à une String directement. La représentation du temps pourra faire l’objet d’un article futur… Outre cela, on commence à être habitué à cette construction de classe.
Article “placeholder”
Enfin, ajoutons des commentaires à l’article que nous avions créé dans la partie précédente. Dans placeholder_data.dart, ajoutez le code suivant :
import 'package:blog_app/model/article.dart';
import 'package:blog_app/model/comment.dart';
import 'package:blog_app/model/profile.dart';[...]String articleComment = [...]String comment =
"Romani callibus pontem ubi attonitus id videretur multis cogitationibus suspendere callibus ambigebat ad inpossibile Romani multitudine prope ambigebat inpossibile convolantibus Romani itaque multis multis supercilia nive ambigebat conpage cum ubi.";
Profile placeholderProfile = new Profile(
username: "adorable",
image: new Image.asset("assets/images/profile_pic.png"));
Comment placeholderComment = new Comment(
content: comment,
timeSinceComment: "4 hours ago",
profile: placeholderProfile);Article placeholderArticle = new Article(
title: "The dark side of the digital nomad",
author: "Mark Manson",
tag: "Travel",
text: articleContent,
image: new Image.asset("assets/images/article_image.jpg", fit: BoxFit.cover),
// Nouvelle ligne ici
comments: new List<Comment>.generate(30, (int index) => placeholderComment));
Pour résumer nos ajouts, on a importé les fichiers des classes que nous venons de construire, et nous avons créés trois nouveaux objets.
Notez une méthode très pratique ici, qui n’a pas d’équivalent en Swift : la méthode generate. Elle nous permet très efficacement de doter notre article de quelques commentaires.
Il me manque plus qu’à modifier article.dart pour que notre objet Article puisse recevoir des commentaires. Voici le code complet, notez simplement l’ajout de l’attribut comments:
import 'package:meta/meta.dart';
import 'package:flutter/material.dart';
import 'package:blog_app/model/comment.dart';
class Article {
String title;
String author;
String tag;
String text;
Image image;
List<Comment> comments;
Article(
{@required this.title,
@required this.author,
@required this.tag,
@required this.text,
@required this.image,
@required this.comments});
}
Styles de texte
Afin d’éviter les aller-retours, nous allons également en profiter pour créer une bonne fois pour toute les styles de texte que nous allons utiliser dans cette partie. Dans text_styles.dart, ajoutez le code suivant :
TextStyle subtitleTextStyle = new TextStyle(
fontFamily: 'Gotham',
fontWeight: FontWeight.w500,
fontSize: 16.0,
color: Colors.black,
letterSpacing: 1.2);
TextStyle commentTilePrimaryStyle = new TextStyle(
fontFamily: 'Gotham',
fontWeight: FontWeight.w400,
fontSize: 14.0,
color: Colors.black,
);
TextStyle commentTileSecondaryStyle = new TextStyle(
fontFamily: 'Gotham',
fontWeight: FontWeight.w400,
fontSize: 14.0,
color: Colors.grey,
);
TextStyle contentTextStyle = new TextStyle(
fontFamily: 'Caecilia',
fontWeight: FontWeight.w400,
fontSize: 14.0,
color: Colors.black.withOpacity(0.7),
height: 2.0);
TextStyle articleContentTextStyle = new TextStyle(
fontFamily: 'Caecilia',
fontWeight: FontWeight.w400,
fontSize: 18.0,
color: Colors.black.withOpacity(0.7),
height: 2.2);
Bien, avec ces ajouts, on peut à présent attaquer les modifications de l’interface de notre application.
Article scrollable / UIScrollView
D’abord, on va voir comment mettre en place un écran scrollable avec Flutter. Pour ce faire, on va placer le contenu de l’article en-dessous du header que l’on a déjà construit. Ci-dessous une vidéo de ce que ça va donner :
Quelles modifications va-t-on devoir faire ?
- L’image de l’article doit rester fixe
- Le contenu de l’article doit se trouver en-dessous du titre
Cela implique donc de changer un petit peu la hiérarchie des widgets par rapport à ce que nous avions conçu dans la partie précédent. C’est à dire que l’on va en réalité créer un nouveau widget qui correspondra à tout notre article (image de fond + titre + contenu texte), dans lequel on aura un autre widget personalisé qui contiendra notre colonne de titre / auteur / tag.
Cette division en plusieurs widgets, donc en plusieurs classes, et donc en plusieurs fichiers est totalement optionnelle. Il serait tout à fait possible de coder notre page de blog dans la fonction build() de notre page ArticlePage, ou bien de répartir le code en plusieurs fonctions, mais au sein de la même classe.
En Swift, fait de manière bien structurée, cela aurait du sens. Cependant, le problème avec Flutter tient plus dans la manière dont se construit le code : non pas en cascade comme en Swift, mais de manière imbriquée. Ce qui fait qu’en multipliant les niveaux de hiérarchie (et également les ramifications), le code devient totalement indigeste tant il est dense. C’est pourquoi il est plus que recommandable de découper le code.
Voilà donc ce que je vous propose :
- ArticlePage sera composé de la barre de navigation que nous avions conçues (remarquez que si cette barre était utilisée en plusieurs endroits, il serait intéressant de l’extraire dans sa propre classe), et d’un widget que l’on va appeler ArticleContent.
- ArticleContent sera quant à lui composé d’une image de fond, d’un scrollable avec en premier élément, notre colonne de texte qui sera ArticleCover, et une zone de texte.
- On en déduit donc que l’on va simplifier ArticleCover pour ne plus consister qu’en la colonne de texte, et on va aussi regarder ce qui se passe d’un point de vue du layout lorsque l’on fait ça.
Quitte à faire quelques aller-retours dans les fichiers, on va découper le travail en étapes de telles manière que vous puissiez toujours utiliser cette belle fonctionnalité qu’est le “Hot Reload”.
ArticleContent, acte 1
Commençons par créer notre nouveau widget ArticleContent, qui sera un enfant quasiment direct de notre page ArticlePage.
Dans le dossier widgets, créez un nouveau fichier article_content.dart dans lequel on va insérer le code suivant :
import 'package:flutter/material.dart';
import 'package:blog_app/model/article.dart';
class ArticleContent extends StatelessWidget {
final Article _article;
ArticleContent(this._article);
@override
Widget build(BuildContext context) {
return new Stack(
fit: StackFit.expand,
children: <Widget>[
_article.image,
new Container(color: Colors.black.withOpacity(0.2)),
],
);
}
}
On retrouve donc bien le début de la description de notre widget, avec une pile (étendue sur toute la surface disponible, qui dépendra du parent) contenant notre image d’article, ainsi qu’un petit Container dont le but est juste d’assombrir un peu l’image pour rendre les textes plus visibles.
Dans article_page.dart, remplaçons alors dans le build() notre ArticleCover pour notre ArticleContent, en l’oubliant pas d’importer le fichier nécessaire pour accéder à ce nouveau widget.
import 'package:blog_app/widgets/article_content.dart';
[...]class ArticlePage extends StatelessWidget {[...]
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Column(
children: <Widget>[
new Expanded(
child: new ArticleContent(_article),
),
_buildBottomBar(),
],
));
}
}
Ainsi, le Stack, qui est le premier objet composant ArticleContent, va prendre tout l’espace disponible dans le widget Expanded, c’est à dire tout l’espace de l’écran qui n’est pas occupé par la barre de navigation. Toujours sans avoir à explicitement poser les contraintes, voilà qui accélère bien le flux de développement.
Bon… On a une image, mais on voulait avoir le reste de l’article par dessus, n’est-ce pas ?
ArticleCover
Pour ce faire, on va donc modifier notre widget ArticleCover pour qu’il ne soit composé que d’une colonne de texte.
C’est à dire tout simplement que, dans article.cover.dart, on va retirer le Stack et l’image, de telle manière que la liste des textes devient directement ce que l’on retourne.
class ArticleCover extends StatelessWidget {
[...]
@override
Widget build(BuildContext context) {
return _buildArticleTextList();
}
}
A ce stade, on pourrait placer le contenu de _buildArticleTextList() (pour rappel, une fonction privée puisque son nom est précédé d’un underscore) dans build(). Ne le faîtes pas, pour une raison que je vous expliquerait très bientôt dans cet article.
Bien, il ne reste plus qu’à replacer de widget dans ArticleContent, construire le texte pour le contenu de l’article, et on aura terminé.
ArticleContent, acte 2
Retournons donc dans ArticleContent pour y insérer un ArticleCover, et un nouveau Text.
C’est également à ce moment que l’on va intégrer l’équivalent d’un UIScrollView côté Flutter, le widget SingleChildScrollView.
Voici le code dans article_content.dart :
import 'package:blog_app/widgets/article_cover.dart';
import 'package:blog_app/ui_model/text_styles.dart';
[...]class ArticleContent extends StatelessWidget {
final Article _article;
ArticleContent(this._article);
@override
Widget build(BuildContext context) {
return new Stack(
fit: StackFit.expand,
children: <Widget>[
_article.image,
new Container(color: Colors.black.withOpacity(0.2)),
// -------- Insertion à partir d'ici -------
// ¬ TEXT CONTENT
new SingleChildScrollView(
// Avoid bouncing
physics: new ClampingScrollPhysics(),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// ¬ COVER
new ArticleCover(_article),
// ¬ ARTICLE CONTENT
new Container(
color: Colors.white,
child: new Padding(
padding: const EdgeInsets.all(32.0),
child: new Text(
_article.text,
style: articleContentTextStyle,
), // Text
), // Padding
) // Container
], // <Widget>
), // Column
) // SingleChildScrollView
],
);
}
}
Alors, on a fait beaucoup de choses ici. Reprenons morceau par morceau ce que l’on a écrit :
- Au-dessus de l’image et du filtre, dans le Stack, on a ajouté un SingleChildScrollView. Comme le Stack est configuré pour étendre chacun de ses enfants le plus possible, la zone scrollable recouvrira bien toute la surface.
- Contrairement à iOS où l’insertion de plusieurs écrans en cascade dans un UIScrollView aurait demandé bien plus de code, ici, tout se joue à quelques lignes.
- physics : définit le comportement du scroll. Faut-il faire un rebond lorsque l’écran arrive tout en haut ou tout en bas ? Dans notre projet, on ne le souhaite pas. De fait, on attribut au widget une physique particulière, et le problème est réglé.
- Comme son nom l’indique, SingleChildScrollView ne prend… qu’un seul enfant. Par conséquent, comment aligner plusieurs widgets les uns en dessous des autres ? En les plaçant tout simplement dans une Column, qui sera l’enfant direct du ScrollView. Et tout le layout se fait en fonction de ça.
- Le layout de la Column est géré par le paramètre crossAxisAlignment, ici réglé de telle manière que les sous-widgets soient le plus large possible.
On a donc deux enfants dans notre Column, une instance d’ArticleContent que nous avons créé plus tôt, et le contenu de l’article, c’est à dire un Text que l’on a décoré un peu en le plaçant dans un encart, lui-même placé dans un Container. La méthode pour composer cet ensemble reste toujours la même, en imbriquant les objets les uns dans les autres.
Rechargeons notre projet pour voir … que ce n’est pas encore ça. En effet, on aimerait bien que notre header d’article remplisse toute la surface de la page lorsque l’écran apparait.
ArticleContent, acte 3
C’est à ce moment que l’on doit rajouter un tout petit peu de contraintes manuellement, mais d’une manière beaucoup plus efficace et claire que côté iOS.
En effet, la taille de notre header dépend en réalité de la taille de l’écran sur lequel on l’affiche.
Côté iOS, il aurait fallu capturer cette valeur après le chargement du UIViewController, lorsque la taille de la fenêtre est définie, modifier la valeur de la constante de hauteur du header, et reconstruire le layout en prenant en compte cette modification de contrainte.
Côté Flutter, il s’avère que la taille de l’écran est une valeur à laquelle on peut accéder à tout moment, et surtout avant de construire l’arborescence de widgets. De fait, il s’agit beaucoup plus simplement d’envoyer un paramètre de hauteur à notre ArticleCover, et le tour est joué.
Voyons dans article_content.dart comment obtenir cette valeur :
class ArticleContent extends StatelessWidget {
[...]
@override
Widget build(BuildContext context) {
final double coverHeight = MediaQuery.of(context).size.height - 70.0;[...]
}
}
On a dit qu’il nous fallait cette valeur à chaque fois que l’on reconstruit le widget, donc à chaque fois que build() est appelée. Donc, avant de retourner quoique ce soit, on accède au paramètre size de MediaQuery qui nous permet de récupérer toute sorte de données concernant l’écran sur lequel va être dessiné le widget.
Il s’agit donc maintenant de modifier article_cover.dart pour qu’il reçoive et agisse en fonction de ce paramètre :
class ArticleCover extends StatelessWidget {
final Article _article;
final double _height;
ArticleCover(this._article, this._height);
[...]
@override
Widget build(BuildContext context) {
return new Container(
height: _height,
child: _buildArticleTextList(),
); // Container
}
}
Le constructeur d’ArticleCover attend donc le nouveau paramètre _height, que l’on utilise en imbriquant notre liste de textes dans un Container, auquel on attribut la hauteur que l’on a reçue.
Ne reste plus qu’à modifier dans article_content.dart la création d’une nouvelle instance d’ArticleCover dans le build() pour lui donner ce paramètre. Repérez la ligne en question et remplacez-là par :
new ArticleCover(_article, coverHeight)
Et le tour est joué :
Remarquez d’ailleurs que ça fonctionne très bien même en format paysage (activé par défaut) :
Page des commentaires / UITableView
Bien, il est temps pour nous de créer une nouvelle page qui nous permettra d’afficher les commentaires du blog que l’on affiche, page à laquelle on accèdera en touchant le bouton commentaire que l’on a implanté dans l’article précédent.
On va donc commencer par coder la nouvelle page CommentsPage, et pour nous aider dans le flux de développement, on va modifier notre main pour faire en sorte que ce soit la nouvelle page qui soit affiché au lancement de l’application. Une fois que celle-ci sera terminée, on s’occupera de la navigation entre les deux écrans.
Dans le dossier pages, créons un nouveau fichier comments_pages.dart, qui contiendra les premiers éléments de la nouvelle page. C’est une page qui ne dépend pas d’un état, puisque l’utilisateur ne pourra pas interagir avec les commentaires dans ce projet. Ce sera donc un StatelessWidget.
import 'package:flutter/material.dart';
import 'package:blog_app/ui_model/text_styles.dart';
import 'package:blog_app/model/comment.dart';
class CommentsPage extends StatelessWidget {
final List<Comment> _comments;
CommentsPage(this._comments);
@override
Widget build(BuildContext context) {
return null;
}
}
C’est une syntaxe familière à présent. On ajoute un attribut immuable qui contiendra la liste de commentaires que l’on voudra afficher.
Et pour pouvoir travailler sur cette page, il faudra qu’elle s’affiche en premier. Voici donc à quoi ressemble notre main.dart pour le permettre :
import 'package:blog_app/pages/comments_page.dart';
import 'package:blog_app/injector/placeholder_data.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
title: "Blog app",
home: new CommentsPage(ArticleInjector.shared.article.comments)));
}
De cette manière, on modifie très simplement le comportement de notre application en elle-même pour faciliter le processus de développement. C’est extrêmement pratique et rapide à faire.
Analyse de l’écran
Parce que tout ce qui touche à l’interface se compose avec le code, et parce que l’on n’a pas d’outil graphique pour nous aider tel que le Storyboard côté iOS, avant de commencer à écrire quoique ce soit, il faut toujours analyser précisément la maquette que l’on a pour en déduire la hiérarchie des éléments.
Notre écran sera donc constitué de deux éléments dans une Column :
- en rouge, un Container à hauteur fixe qui contiendra notre barre de navigation
- en bleu, la liste des commentaires scrollable, une ListView, qui devra prendre tout le reste de l’espace disponible, et donc qui devra être inclus dans un Expanded. Cette ListView contiendra des cellules, des widgets comme les autres, contrairement à iOS où il faudrait utiliser un héritier de UITableViewCell, on y reviendra.
Voilà pour le layout général.
Observons de plus près notre barre de navigation :
Bon, c’est un petit peu plus dense, mais découpons cela niveau par niveau :
- en orange, le Container principal qui contiendra tout le reste. On lui attribuera un padding que quelques pixels symétriquement sur les côtés gauche et droits pour que le reste du contenu ne soit pas collé au bords de l’écran.
- en vert, ce sera un Stack. La raison à cela est que l’on veut à la fois centrer le label “Comments (30)”, et avoir le bouton tout à droite. On va donc devoir empiler l’un au dessus de l’autre deux objets, à savoir :
- en rouge, le Text qui sera inclus dans un Center pour se trouver, vous l’aurez deviné, au centre du Stack et donc de la barre de navigation,
- en bleu, on souhaiterai placer le bouton le plus à droite possible. Il nous faudra donc construire une Row, composé du bouton et d’un Container étendu le plus possible (donc dans un Expanded) pour contraindre le bouton à se placer à droite.
Beaucoup de texte pour expliquer comment cette petite partie de l’écran va s’organiser, mais en terme de code, vous conviendrez, je pense, du fait que cela est bien plus explicite que de poser manuellement des contraintes à la syntaxe très dense comme on le ferait côté iOS.
Pour les cellules de la ListView, je vous propose le découpage suivant :
On va pouvoir passer un peu plus rapidement sur cette partie, car on retrouve finalement tout le temps les mêmes objets et les mêmes schémas d’imbrication. Notons donc les points suivants :
- pour ajouter de l’espace entre les cellules et avec les bords de l’écran, on place tout le contenu d’une cellule dans un Padding (en orange)
- l’enfant direct du Padding sera une Row. Il faudra simplement la régler pour que ses enfants soient alignés suivant leur bord supérieur, afin que l’image se trouve dans le coin gauche supérieur.
- en bleu foncé, un petit Padding horizontal pour créer de l’espace entre l’image et les textes
- la zone verte sera une Column de trois éléments, une Row en bleu clair (car composée de 3 éléments séparés par des Padding horizontaux), un Padding vertical, et en rose un dernier Text qui donnera sa hauteur à la cellule entière.
Avec cet exemple, on a un bon résumé de la manière de composer une interface et de disposer correctement tous les éléments avec Flutter.
On peut maintenant commencer à programmer cette interface.
Barre de navigation
Commençons par la barre de navigation en haut de CommentsPage. Dans comments_page.dart, ajoutez ce morceau de code à la classe CommentsPage :
Widget _buildTopBar() {
return new Container(
height: 70.0,
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
color: Colors.white,
child: new Stack(children: <Widget>[
// ¬ "COMMENTS" LABEL
new Center(
child: new Text(
"COMMENTS (${_comments.length})",
style: subtitleTextStyle,
),
),
// ¬ CLOSE BUTTON
new Row(
children: <Widget>[
new Expanded(child: new Container()),
new IconButton(
icon: new Icon(Icons.close),
onPressed: () => print("Comments button touched")),
],
),
]),
);
}
Ce morceau de code est simplement la traduction de la décomposition que l’on a faite plus haut lors de l’analyse de la maquette. Notez également quelques petits éléments que vous pourrez retrouver souvent avec Dart :
- la syntaxe d’une string interpolation dans le label “COMMENTS (…)”, pour afficher entre parenthèses le nombre de commentaires qui sont affichés.
- une fonction lambda passée en paramètre du callback de notre bouton, qui permet de grandement simplifier l’écriture lorsqu’une seule action doit être effectuée.
A présent, modifions notre fonction build() pour utiliser le widget que l’on vient de coder. Remplacez le contenu de la fonction par ce qui suit dans comments_page.dart :
@override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.white,
body: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Padding(padding: const EdgeInsets.only(top: 15.0)),
_buildTopBar(),
new Expanded(child: new Container()),
], // <Widget>
) // Column
); // Scaffold
}
On a placé notre barre de navigation dans une Column. En premier élément de celle-ci, un petit Padding vertical pour éviter la superposition de la barre de navigation de l’app et la barre de statut du smartphone (Notez accessoirement que celle-ci disparait en mode paysage, pratique). En dernier élément, un Container étendu qui sera remplacé par notre ListView.
Justement, construisons-là, et commençons par construire les cellules dont sera composée la liste.
CommentTile
Contrairement à iOS où une cellule de TableView doit hériter de UITableViewCell, avec Flutter, puisque tout est widget, une cellule de ListView n’est également qu’un widget.
Dans le dossier widgets, créons un nouveau fichier comment_tile.dart qui contiendra le code suivant :
import 'package:flutter/material.dart';
import 'package:blog_app/model/comment.dart';
class CommentTile extends StatelessWidget {
final Comment _comment;
CommentTile(this._comment);
Widget _buildAvatar() {
return new Container(
height: 50.0,
width: 50.0,
child: _comment.profile.image,
);
}
Widget _buildCommentInfo() {
return new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// ¬ USERNAME
new Text(
_comment.profile.username,
),
new Padding(padding: const EdgeInsets.only(left: 10.0)),
// ¬ SEPARATOR
new Container(
height: 3.0,
width: 3.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey,
),
),
new Padding(padding: const EdgeInsets.only(left: 10.0)),
// ¬ TIME SPENT SINCE PUBLICATION
new Text(
_comment.timeSinceComment,
),
],
),
new Padding(padding: const EdgeInsets.only(top: 12.0)),
// ¬ TEXT CONTENT
new Text(
_comment.content,
)
],
);
}
@override
Widget build(BuildContext context) {
return new Container(
margin: const EdgeInsets.only(
top: 6.0, bottom: 40.0, left: 35.0, right: 20.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildAvatar(),
new Padding(padding: const EdgeInsets.only(left: 20.0)),
new Expanded(
child: _buildCommentInfo(),
)
],
),
);
}
}
Il s’agit là du code intégral pour constituer notre cellule. Je vous propose de le décomposer rapidement en reprenant l’image de notre maquette initiale :
- _buildAvatar() : construit la zone rouge, c’est à dire en donnant un cadre à notre image.
- _buildCommentInfo() : construit toute la zone en vert en colonne. La Row représente la zone en bleu. Remarquez que le point est un Container auquel on a donné une petite taille, une forme circulaire et une couleur.
- build() : construit l’ensemble de la cellule. Remarquez qu’au-lieu d’utiliser la propriété padding de Container, on utilise margin. De cette manière, les marges sont situées à l’extérieur du Container. Avec padding, les marges seraient situées entre les bords du Container et ses enfants.
ListView
Côté iOS, il nous faudrait utiliser plusieurs méthodes définies par UITableViewDelegate et UITableViewDataSource afin de construire et de peupler un TableView.
Avec Flutter, tout se fait en utilisant la fonction build(), parce que l’arbre des widgets est redessiné à chaque fois que le contenu change.
Dans comments_page.dart, insérez le code suivant à la classe CommentsPage :
[...]
import 'package:blog_app/widgets/comment_tile.dart';
class CommentsPage extends StatelessWidget {
[]
Widget _buildCommentsList() {
return new ListView.builder(
itemCount: _comments.length,
itemBuilder: (BuildContext context, int index) {
final Comment comment = _comments[index];
return new CommentTile(comment);
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.white,
body: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Padding(padding: const EdgeInsets.only(top: 15.0)),
_buildTopBar(),
new Expanded(child: _buildCommentsList()),
],
));
}
}
Pour construire une ListView, on utilise son constructeur builder, que l’on va détailler et comparer avec iOS :
- le paramètre itemCount remplace numberOfRowsInSection (mais sans la gestion des sections), auquel on indique, comme vous pouvez le déduire, le nombre de lignes que l’on aura
- le paramètre itemBuilder, qui remplace cellForRowAt, appelé pour chaque widget à construire dans la liste, et qui prend une fonction en paramètre. Cette fonction nous donne deux paramètres : le BuildContext, que l’on n’utilise pas ici, et l’index de l’item qui va être construit, dont on se sert pour sélectionner le commentaire à afficher.
N’oubliez pas de remplacer le Container inclus dans Expanded par notre nouvelle fonction, et rechargez l’application.
On remarquera donc que la construction d’une ListView est très concise en terme de code, a fortiori dans la mesure où on ne fait rien de compliqué dans ce cas de figure.
Navigation entre deux écrans
Il ne reste maintenant plus qu’à permettre à l’utilisateur d’ouvrir la page des commentaires depuis celle de l’article.
Contrairement à iOS, où il faudrait déclencher la navigation depuis le ViewController actuellement présenté, avec Flutter, comme tout est widget, chaque objet peut déclencher une navigation entre plusieurs écrans. Voyons voir comment cela se met en place :
Dans article_page.dart, on va ajouter à ArticlePage la fonction suivante :
import 'package:blog_app/pages/comments_page.dart';
class ArticlePage extends StatelessWidget {
[...]
void _presentComments(BuildContext context) {
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) =>
new CommentsPage(_article.comments)));
}
[...]
}
Lorsque l’on va appeler cette fonction, la nouvelle page CommentsPage va être créée, d’après ce que l’on a codé dans le paramètre builder, et une transition animée sera faite entre les deux écrans. Puisque l’on utilise une MaterialPageRoute, l’animation est définie comme étant l’animation usuelle des applications en Material Design.
Notez que l’on aurait pu placer une telle fonction dans n’importe quelle classe de widget, comme par exemple dans CommentTile, quand bien même cet objet ne soit pas une page en elle-même.
Il faut également modifier _buildBottomBar() et build() pour y passer le paramètre de BuildContext :
Widget _buildBottomBar(BuildContext context) {
[...]
new IconButton(
icon: new Icon(Icons.comment),
onPressed: () => _presentComments(context)),
[...]
}
@override
Widget build(BuildContext context) {
return new Scaffold(
[...]
_buildBottomBar(context)
[...]
));
}
Enfin, rétablissons notre main.dart pour afficher l’article comme premier écran :
void main() {
runApp(new MaterialApp(
title: "Blog App",
home: new ArticlePage(ArticleInjector.shared.article),
));
}
Et si on relance l’application, on peut maintenant naviguer entre les deux écrans :
Il faut noter une chose : par défaut, lorsqu’une page est push depuis une autre, il est possible de retourner à la page d’origine avec un slide du bord gauche de l’écran vers le centre. Mais le bouton “croix” que nous avons implanté n’est pas encore fonctionnel.
Dans comments_page.dart, c’est encore plus simple à implanter :
class CommentsPage extends StatelessWidget {
[...]
void _dismissComments(BuildContext context) {
Navigator.pop(context);
}
Widget _buildTopBar(BuildContext context) {
[...]
new IconButton(
icon: new Icon(Icons.close),
onPressed: () => _dismissComments(context)),
[...]
}
[...]
@override
Widget build(BuildContext context) {
[...]
_buildTopBar(context),
[...]
}
}
Et voila très simplement comment actionner la transition inverse : en utilisant la méthode pop() de Navigator. Ce qui nous donne le résultat suivant :
Le système de navigation n’est pas révolutionnaire en ce sens qu’il ne diffère pas beaucoup de ce que l’on connait de manière générale. Cependant, la possibilité de pouvoir actionner des transitions depuis n’importe quel widget est un vrai plus, qui cependant peut aussi amener des difficultés lorsqu’il s’agit de manipuler et de passer des données. A voir dans des implantations plus complètes.
Conclusion
Voici le résultat final que nous avons obtenu grâce au travail que nous avons réalisé dans ces deux articles :
On a donc vu dans cet article plus précisément :
- l’implantation d’une zone scrollable équivalente au ScrollView,
- l’implantation d’une liste scrollable de widgets équivalente au TableView,
- le processus de disposition des différents widgets et la mise en place automatique des contraintes en fonction de l’imbrication de certains objets dans d’autres,
- le processus de navigation simple entre deux écrans.
A travers ce petit projet très axé interface, j’espère que vous avez pu en apprendre plus sur le fonctionnement de Flutter et percevoir les différences qu’il y a entre le développement iOS natif et ce nouveau framework.
A nouveau, il ne s’agit pas d’une comparaison proprement dite, mais plutôt d’une mise en relief des différences dans l’approche à adopter et de l’adaptation nécessaire pour passer d‘un paradigme à l’autre.
Voilà !
Si vous avez des suggestions à faire, ou bien si vous trouvez une erreur quelconque, ou même si vous avez simplement apprécié l’article, n’hésitez pas à me le mentionner.
Le projet final
Vous pouvez retrouver le projet final ici.