Bienvenue dans ce nouveau tuto Flutter WhatsApp, dans lequel nous allons redessiner la célèbre application de messagerie mobile.

Nous allons voir comment recréer l’interface utilisateur de WhatsApp avec Flutter tout en détaillant au maximum les notions de design abordées.

L’application sera constituée de deux pages, une page d’accueil pour afficher les contacts favoris et les conversations, et une page de messagerie avec les messages à afficher.

L’ensemble de l’application a été redessinée pour proposer de nouvelles courbes et un thème couleur ajusté pour WhatsApp:

Pour réaliser ce tuto Flutter WhatsApp, nous suivrons les étapes suivantes:

  1. Création et configuration de l’application
  2. Design de la page d’accueil (qui liste les conversations)
  3. Design de la page de discussion (chat)

N’hésitez pas à télécharger le code source de l’application pour la tester rapidement sur un émulateur.

Tuto Flutter WhatsApp | 1. Création et configuration de l’application

Dans cette partie nous allons créer un projet Flutter et configurer les éléments principaux de notre application, comme les ressources et les packages importer.

1.1. Création du projet avec la commande Flutter

Ici nous allons tout simplement utiliser la commande Flutter permettant de créer un nouveau projet et ensuite préparer notre application.

flutter create whatsapp

Une fois que le projet est créé, nous pouvons l’ouvrir avec notre éditeur VS Code afin de le modifier.

On commence par se rendre dans le fichier principal main.dart et à supprimer tout son contenu, pour construire notre design en partant de zéro.

1.2 Importation des photos

Dans notre application, les images seront utilisées comme photo de profil ou comme pièce jointe dans le chat.

Pour cela, nous allons créer un dossier images dans lequel nous allons mettre les photos dont nous aurons besoin.

Ce dossier contiendra donc un autre sous dossiers avatars comportant les photos de profil que nous utiliserons dans toute notre application.

Tuto Flutter WhatsApp

Ensuite nous nous rendons dans notre fichier pubspec.yaml afin de déclarer les dossiers comportant nos images comme ci-dessous :

assets:
    - images/avatar/
    - images/

Nous entrons ensuite la commande suivante pour prendre en compte l’ajout des images dans notre application :

flutter pub get

1.3 Installation du package Google Fonts

Pour ce tutoriel, nous allons utiliser le package google_fonts qui donne accès à toutes les polices de Google: https://pub.dev/packages/google_fonts

Pour cela, nous devons ajouter la dépendance du package google_fonts dans le fichier pubspec.yaml comme ceci:

google_fonts: ^2.1.0

Une fois le package importé, enregistrez le fichier et le téléchargement devrait se faire automatiquement, sinon entrez la commande suivante:

flutter pub get

Nous pourrons ensuite importer nos deux packages en utilisant le code dart suivant:

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

Nos deux packages de base étant importés, nous allons commencer à construire notre application Flutter.

1.4 Déclaration de la fonction main() et de la classe MyApp

Tout d’abord, nous allons commencer par déclarer la fonction main() dans notre fichier main.dart.

C’est elle qui lancera le code de notre application, grâce à la fonction runApp() qui chargera la classe MyApp:

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

La classe MyApp va contenir la méthode build() qui permettra de dessiner l’interface utilisateur avec le widget MaterialApp() (application utilisant le material design).

Nous donnons un titre à notre application avec le champ title, et nous mettons également l’attribut debugShowCheckedModeBanner à false pour supprimer la bannière de débogage.

Enfin nous avons le champ home qui va nous permettre de définir le Widget qui sera la page d’accueil de l’application.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'WhatsApp Redesign',
      home: HomePage(),
    );
  }
}

Ici nous appelons le widget HomePage() qui représentera notre page d’accueil que nous allons créer dans la prochaine partie.

Pour finir, nous définissons au début de notre fichier des variables de couleurs utilisées dans toute notre application:

const dGreen = Color(0xFF2ac0a6);
const dWhite = Color(0xFFe8f4f2);
const dBlack = Color(0xFF34322f);

La première couleur est définie en utilisant un code Hexadécimal pour retrouver la teinte de vert bien précise de notre design.

Tuto Flutter WhatsApp

Tuto Flutter WhatsApp | 2. Création de la page d’accueil

Dans cette deuxième partie de ce tutoriel Flutter WhatsApp, nous allons créer le design de notre page d’accueil en détaillant les différentes sections qui la composent.

La page d'accueil sera constituée en grande partie de la liste des conversations et d'un menu de navigation.

2.1. Architecture de la HomePage

Nous allons diviser notre page en plusieurs widgets qui seront créés indépendamment avant d’être appelés dans notre HomePage().

Pour commencer, nous déclarons notre classe HomePage() comme un StatelessWidget qui va contenir la page d’accueil de notre application.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
      	title: Text('WhatsApp'),
      ),
      body: Container(),
    );
  }
}

La méthode build retourne un Scaffold(), le widget qui implémente la structure de base d’une page en utilisant le Material Design.

Il nous permettra de définir tous les éléments de notre page, à savoir la appBar, le body et le bouton flottant avec le champ floatingActionButton.

2.2 Création de la barre de navigation (appBar)

Notre appBar est constitué de deux icônes que nous allons créer en utilisant le widget IconButton.

Tuto Flutter WhatsApp

Dans la déclaration de notre appBar, nous allons aussi renseigner les propriétés nécessaires à notre widget AppBar afin de le personnaliser comme sur notre design de base.

Nous plaçons notre première icône dans le champ leading grâce au widget IconButton.

Ensuite, nous définissons l'icône de recherche dans le champ actions comme vous pouvez le voir dans le code ci-dessous :

appBar: AppBar(
  elevation: 0,
  backgroundColor: dBlack,
  leading: IconButton(
    onPressed: () {},
    icon: const Icon(
      Icons.menu,
      color: dWhite,
      size: 30,
    ),
  ),
  actions: [
    IconButton(
      onPressed: () {},
      icon: const Icon(
        Icons.search_rounded,
        color: dWhite,
        size: 30,
      ),
    ),
  ],
),  

2.3 Ajout du bouton flottant (FloatingActionButton)

Nous ajoutons également un bouton flottant à notre page d'accueil, pour rester fidèle au design de WhatsApp.

Tuto Flutter WhatsApp

Avec Flutter, nous affichons le FloatingActionButton dans le champ floatingActionButton de notre Scaffold .

Ce FloatingActionButton est ainsi placé à l'extrémité de notre page en bas à droite:

floatingActionButton: FloatingActionButton(
  onPressed: () {},
  backgroundColor: dGreen,
  child: const Icon(
    Icons.edit,
    size: 20,
  ),
),

2.4 Création de la MenuSection

Maintenant que notre appBar est définie, nous allons passer au contenu de notre page à construire qui sera subdivisé en plusieurs sections.

La première section (MenuSection) sera constituée des onglets de notre page d'accueil, la deuxième (FavoriteSection) des contacts favoris et la dernière (MessagesSection) des différentes discussions.

Ci-dessous, le code complet du champ body de notre HomePage qui fait appel aux sections que nous allons déclarer :

body: Column(
  children: [
    MenuSection(),
    FavoriteSection(),
    Expanded(
      child: MessageSection(),
    )
  ],
),

Le corps de notre page d’accueil est constitué du widget Column pour permettre d'aligner son contenu à la verticale.

Le premier élément de notre Column est la MenuSection, qui retourne un Container comportant une seule Row.

Tuto Flutter WhatsApp

Pour faire défiler notre Menu à la horizontale, nous allons également utiliser le widget SingleChildScrollView.

Ensuite nous définissons l'attribut scrollDirection avec comme valeur Axis.horizontal, afin que le défilement se fasse de façon horizontale:

class MenuSection extends StatelessWidget {
  final List menuItems = ["Message", "Online", "Groups", "Calls"];
  MenuSection({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      color: dBlack,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(15, 15, 15, 25),
          child: Row(
            children: menuItems.map((item) {
              return Container(
                margin: const EdgeInsets.only(right: 55),
                child: Text(
                  item,
                  style: GoogleFonts.inter(
                    color: Colors.white60,
                    fontSize: 29,
                  ),
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

Pour afficher les différents onglets, nous parcourrons notre liste menuItems et affichons pour chaque item un simple Container avec du texte.

2.5 Création de la FavoriteSection

Cette section est la deuxième de notre Column, et nous permettra de scroller toujours à l'horizontale nos contacts favoris.

Tuto Flutter WhatsApp

Elle va nous permettre d'afficher la photo de profil de chacun des contacts, ainsi que son nom exactement comme sur le nouveau design de WhatsApp:

class FavoriteSection extends StatelessWidget {
  FavoriteSection({Key? key}) : super(key: key);
  final List favoriteContacts = [
    {
      'name': 'Alla',
      'profile': 'images/avatar/a1.jpg',
    },
    [...],
    {
      'name': 'Steve',
      'profile': 'images/avatar/a7.jpg',
    },
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      color: dBlack,
      child: Container(
        padding: const EdgeInsets.symmetric(vertical: 15),
        decoration: const BoxDecoration(
          color: dGreen,
          borderRadius: BorderRadius.only(
            topRight: Radius.circular(40),
            topLeft: Radius.circular(40),
          ),
        ),
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Container(
                  margin: const EdgeInsets.only(left: 15),
                  child: Text(
                    "Favorite contacts",
                    style: GoogleFonts.inter(
                      color: Colors.white,
                      fontSize: 14,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
                const IconButton(
                  icon: Icon(
                    Icons.more_horiz,
                    color: Colors.white,
                    size: 20,
                  ),
                  onPressed: null,
                ),
              ],
            ),
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: favoriteContacts.map((favorite) {
                  return Container(
                    margin: const EdgeInsets.only(left: 15),
                    child: Column(
                      children: [
                        Container(
                          padding: const EdgeInsets.all(4),
                          height: 70,
                          width: 70,
                          decoration: const BoxDecoration(
                            color: dWhite,
                            shape: BoxShape.circle,
                          ),
                          child: CircleAvatar(
                            radius: 20,
                            backgroundImage: AssetImage(favorite['profile']),
                          ),
                        ),
                        const SizedBox(height: 6),
                        Text(
                          favorite['name'],
                          style: GoogleFonts.inter(
                            color: Colors.white,
                            fontSize: 14,
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                      ],
                    ),
                  );
                }).toList(),
              ),
            )
          ],
        ),
      ),
    );
  }
}

2.6 Création de la MessagesSection

On continue avec la MessageSection qui nous permet d'afficher nos différentes conversations avec toutes les informations importantes:

Pour commencer, nous déclarons une List de Map contenant toutes les informations d'une conversation ou d'un message reçu.

Par la suite, nous allons parcourir notre liste pour afficher les différentes informations à leur place dans un widget Column:

class MessageSection extends StatelessWidget {
  MessageSection({Key? key}) : super(key: key);
  final List messages = [
    {
      'senderProfile': 'images/avatar/a2.jpg',
      'senderName': 'Lara',
      'message': 'Hello! how are you',
      'unRead': 0,
      'date': '16:35',
    },
    [...],
    {
      'senderProfile': 'images/avatar/a7.jpg',
      'senderName': 'Stive',
      'message': 'Hello! how are you',
      'unRead': 3,
      'date': '07:31',
    },
  ];

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: messages.map((message) {
          return InkWell(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const ChatPage(),
                ),
              );
            },
            splashColor: dGreen,
            child: Container(
              padding: const EdgeInsets.only(left: 30, right: 10, top: 15),
              child: Row(
                children: [
                  Container(
                    margin: const EdgeInsets.only(right: 23),
                    width: 62,
                    height: 62,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      image: DecorationImage(
                        image: AssetImage(message['senderProfile']),
                        fit: BoxFit.cover,
                      ),
                    ),
                  ),
                  Expanded(
                    child: Column(
                      children: [
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Container(
                              margin: const EdgeInsets.only(top: 25),
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  Text(
                                    message['senderName'],
                                    style: GoogleFonts.inter(
                                      color: Colors.grey,
                                      fontSize: 15,
                                      fontWeight: FontWeight.w500,
                                    ),
                                  ),
                                  Wrap(children: [
                                    Text(
                                      message['message'],
                                      style: GoogleFonts.inter(
                                        color: Colors.black87,
                                        fontSize: 15,
                                        fontWeight: FontWeight.w500,
                                      ),
                                    ),
                                  ]),
                                ],
                              ),
                            ),
                            Column(
                              children: [
                                Text(message['date']),
                                message['unRead'] != 0
                                    ? Container(
                                        padding: const EdgeInsets.all(5),
                                        decoration: const BoxDecoration(
                                          color: dGreen,
                                          shape: BoxShape.circle,
                                        ),
                                        child: Text(
                                          message['unRead'].toString(),
                                          style: GoogleFonts.inter(
                                            color: Colors.white,
                                            fontSize: 12,
                                            fontWeight: FontWeight.w500,
                                          ),
                                        ),
                                      )
                                    : Container(),
                              ],
                            ),
                          ],
                        ),
                        const SizedBox(height: 20),
                        Container(
                          color: Colors.grey[400],
                          height: 0.5,
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          );
        }).toList(),
      ),
    );
  }
}

Chaque ligne (Row) est constitué de deux colonnes, la première contient la photo de profil et la deuxième une autre Row avec le nom de l'expéditeur, un extrait de message et la date.

Pour afficher le message, nous entourons le widget Text par Wrap pour provoquer un retour à la ligne lorsque le texte est trop long.

Enfin, chaque ligne est entourée par le widget InkWell qui va nous permettre de détecter le clique sur la conversation.

Nous allons donc utilisé le champ onTap afin de rediriger vers la page de Chat avec la classe Navigator() de Flutter.

Nous voilà donc rendu au terme de de la création de notre page d’accueil, nous pouvons donc passer à la création de la page de Chat.

Tuto Flutter WhatsApp

Tuto Flutter WhatsApp | 3. Création de la page de Chat

Dans cette partie de notre tuto Flutter WhatsApp, nous allons compléter le design de notre application avec la page de Chat.

Cette page affichera les différents types de messages envoyés entre deux personnes à l'intérieur de leur conversation.

3.1 Création du fichier de la page Chat

Le contenu de la page de Chat sera placé dans un fichier différent, afin de bien organiser notre code et le rendre lisible.

Pour créer notre page de chat, nous allons nous rendre dans le dossier lib et faire un clic droit, puis créer un nouveau fichier que nous allons appeler “chat_page.dart”.

Cette page doit aussi être importée dans notre fichier main.dart afin de permettre la navigation entre les deux pages:

import 'chat_page.dart';
// ou
import 'package:whatsapp/chat_page.dart';

Une fois notre page créée, nous commençons par importer le package Material qui est indispensable à la création de notre contenu Flutter:

import 'package:flutter/material.dart';

3.2 Création de la appBar

On commence ensuite par créer le haut de notre page, avec notamment la AppBar qui est constitué de deux boutons personnalisés.

L'un pour permettre de revenir en arrière à la page d’accueil et l'autre pour avoir plus de détails sur la conversation en cours:

Voilà le code complet de notre appBar constitué de deux IconButton:

appBar: AppBar(
  elevation: 0,
  backgroundColor: dBlack,
  leading: IconButton(
    icon: const Icon(
      Icons.arrow_back_ios,
      color: Colors.white,
      size: 23,
    ),
    onPressed: () {
      Navigator.pop(context);
    },
  ),
  actions: [
    IconButton(
      icon: const Icon(
        Icons.more_vert,
        color: Colors.white,
        size: 23,
      ),
      onPressed: () {},
    ),
  ],
),

3.3 Création de notre bottomNavigationBar

On continue la création de notre page avec l'élément qui se positionne aussi de manière fixe et absolu dans notre écran, la bottomNavigationBar.

C'est dans cette bottomNavigationBar que nous contiendrons le formulaire d'envoi de message, avec un champ texte et différents boutons:

Pour créer notre barre de navigation, nous utilisons le widget classique BottomAppBar() à l'intérieur du champ bottomNavigationBar de notre Scaffold.

Voilà donc le code complet de notre barre de navigation, constituée d'un TextField et de différents IconButton:

class BottomSection extends StatelessWidget {
  const BottomSection({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return BottomAppBar(
      elevation: 10.0,
      color: dWhite,
      child: Container(
        padding: const EdgeInsets.all(20),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Expanded(
              child: Container(
                height: 43,
                decoration: const BoxDecoration(
                  color: dGreen,
                  borderRadius: BorderRadius.all(Radius.circular(30)),
                ),
                child: Row(
                  children: const [
                    SizedBox(width: 10.0),
                    Icon(
                      Icons.insert_emoticon,
                      size: 25.0,
                      color: Colors.white,
                    ),
                    SizedBox(
                      width: 8.0,
                    ),
                    Expanded(
                      child: TextField(
                        decoration: InputDecoration(
                          border: InputBorder.none,
                        ),
                      ),
                    ),
                    Icon(
                      Icons.upload_outlined,
                      size: 25.0,
                      color: Colors.white,
                    ),
                    SizedBox(
                      width: 8.0,
                    ),
                    Icon(
                      Icons.image,
                      size: 25.0,
                      color: Colors.white,
                    ),
                    SizedBox(
                      width: 10.0,
                    ),
                  ],
                ),
              ),
            ),
            Container(
              margin: const EdgeInsets.only(
                left: 25,
              ),
              width: 45,
              height: 45,
              decoration: const BoxDecoration(
                color: dGreen,
                shape: BoxShape.circle,
              ),
              child: const IconButton(
                icon: Icon(
                  Icons.mic_none_sharp,
                  color: Colors.white,
                ),
                onPressed: null,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Notre BottomAppBar contient tout d'abord un Container pour la marge intérieure, mais surtout une Row.

Cette Row va nous permettre de placer à l'horizontal tous les widgets pour l'envoi des messages à la suite les uns des autres.

Tuto Flutter WhatsApp

3.4 Création de la ChatingSection

On continue avec l'implémentation des widgets contenus dans le corps de notre page, et donc dans la ChatingSection.

Le Widget ChatingSection contiendra notamment une SingleChildScrollView pour permettre de scroller les messages à la verticale:

Voilà pour rappel le code de notre body:

body: ChatingSection(),

Cette section retourne donc une Column dans laquelle on listera les différents types de messages envoyés, sachant que chaque type de message possède un widget séparé.

Et voilà le code complet de notre ChatingSection, avec l'entête de texte et la liste de nos messages:

class ChatingSection extends StatelessWidget {
  final String senderProfile = 'images/avatar/a3.jpg';
  final String receiverProfile = 'images/avatar/a6.jpg';
  const ChatingSection({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 15),
      height: double.infinity,
      decoration: const BoxDecoration(
        color: dWhite,
        borderRadius: BorderRadius.only(
          topRight: Radius.circular(40),
          topLeft: Radius.circular(40),
        ),
      ),
      child: SingleChildScrollView(
        child: Column(
          children: [
            const SizedBox(height: 15),
            Text(
              "Alla Burda",
              style: GoogleFonts.inter(
                color: Colors.black87,
                fontSize: 15,
                fontWeight: FontWeight.w600,
              ),
            ),
            Text(
              "Was online 56 seconde ago",
              style: GoogleFonts.inter(
                color: Colors.grey,
                fontSize: 13,
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 45),
            TextMessage(
              message: "Months on ye at by esteem",
              date: "17:19",
              senderProfile: senderProfile,
              isReceiver: 1,
              isDirect: 0,
            ),
            TextMessage(
              message: "Seen you eyes son show",
              date: "17:13",
              senderProfile: senderProfile,
              isReceiver: 0,
              isDirect: 0,
            ),
            TextMessage(
              message: "As tolerably recommend shameless",
              date: "17:10",
              senderProfile: senderProfile,
              isReceiver: 0,
              isDirect: 1,
            ),
            TextMessage(
              message: "She although cheerful perceive",
              date: "17:10",
              senderProfile: senderProfile,
              isReceiver: 1,
              isDirect: 0,
            ),
            const ImageMessage(
              image: 'images/avatar/a1.jpg',
              date: "17:09",
              description: "Least their she you now above going stand forth",
            ),
            AudioMessage(date: "18:05", senderProfile: senderProfile),
            TextMessage(
              message: "Provided put unpacked now but bringing. ",
              date: "16:59",
              senderProfile: senderProfile,
              isReceiver: 1,
              isDirect: 0,
            ),
            TextMessage(
              message: "Under as seems we me stuff",
              date: "16:53",
              senderProfile: senderProfile,
              isReceiver: 0,
              isDirect: 0,
            ),
            TextMessage(
              message: "Next it draw in draw much bred",
              date: "16:50",
              senderProfile: senderProfile,
              isReceiver: 0,
              isDirect: 1,
            ),
            TextMessage(
              message: "Sure that that way gave",
              date: "16:48",
              senderProfile: senderProfile,
              isReceiver: 1,
              isDirect: 0,
            ),
            const SizedBox(height: 15),
          ],
        ),
      ),
    );
  }
}

Pour lister les messages du chat, nous avons créé trois widgets que nous allons détailler dans les parties suivantes:

  • TextMessage
  • ImageMessage
  • AudioMessage

Chacun de ces widgets représente un type de message particulier et permet de bien différencier leur affichage.

3.5 Création du widget TextMessage

On commence avec le premier type qui permettra d'afficher des messages de type texte:

Son constructeur va prendre en compte cinq paramètres qui seront fourni à son widget:

  • message: qui va contenir le texte à envoyer
  • date: pour l'heure d'envoi du message
  • senderProfile: pour le chemin d'accès à la photo de profil de l'expéditeur
  • isReceiver: permet de savoir qui envoie le message afin de bien positionner ses éléments
  • isDirect: permet de savoir si le message envoyé suis directement un message de la même personne

Le code complet du widget TextMessage est le suivant:

class TextMessage extends StatelessWidget {
  final String message, date, senderProfile;
  final int isReceiver, isDirect;

  const TextMessage({
    Key? key,
    required this.message,
    required this.date,
    required this.senderProfile,
    required this.isReceiver,
    required this.isDirect,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 5),
      child: Row(
        children: [
          isReceiver == 1 && isDirect == 0
              ? Container(
                  margin: const EdgeInsets.only(right: 15),
                  width: 45,
                  height: 45,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                    image: DecorationImage(
                      image: AssetImage(senderProfile),
                      fit: BoxFit.cover,
                    ),
                  ),
                )
              : SizedBox(
                  width: 60,
                  child: Row(
                    children: [
                      const Icon(
                        Icons.check,
                        color: dGreen,
                        size: 13.0,
                      ),
                      const SizedBox(width: 7.0),
                      Text(
                        date,
                        style: GoogleFonts.inter(
                          color: dGreen,
                          fontSize: 12,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ],
                  ),
                ),
          Expanded(
            child: Container(
              alignment: Alignment.centerLeft,
              margin: isReceiver == 1
                  ? const EdgeInsets.only(
                      right: 25,
                    )
                  : const EdgeInsets.only(
                      left: 20,
                    ),
              padding: const EdgeInsets.all(6),
              height: 55,
              decoration: isReceiver == 1
                  ? const BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(12),
                        topRight: Radius.circular(12),
                        bottomLeft: Radius.circular(12),
                      ),
                    )
                  : const BoxDecoration(
                      color: dGreen,
                      borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(15),
                        topRight: Radius.circular(15),
                        bottomRight: Radius.circular(15),
                      ),
                    ),
              child: Text(
                message,
                style: GoogleFonts.inter(
                  color: isReceiver == 1 ? dGreen : Colors.white,
                  fontSize: 12,
                  fontWeight: FontWeight.w500,
                ),
              ),
            ),
          ),
          isReceiver == 1 && isDirect == 0
              ? SizedBox(
                  width: 60,
                  child: Row(
                    children: [
                      const Icon(
                        Icons.check,
                        color: dGreen,
                        size: 13.0,
                      ),
                      const SizedBox(
                        width: 7.0,
                      ),
                      Text(
                        date,
                        style: GoogleFonts.inter(
                          color: dGreen,
                          fontSize: 12,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ],
                  ),
                )
              : Container(),
          isDirect == 0 && isReceiver == 0
              ? Container(
                  margin: const EdgeInsets.only(
                    left: 16,
                    right: 10,
                  ),
                  width: 45,
                  height: 45,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                    image: DecorationImage(
                      image: AssetImage(senderProfile),
                      fit: BoxFit.cover,
                    ),
                  ),
                )
              : Container(),
          isReceiver == 0 && isDirect == 1
              ? Container(
                  margin: const EdgeInsets.only(
                    left: 16,
                    right: 10,
                  ),
                  width: 45,
                  height: 45,
                )
              : Container(),
        ],
      ),
    );
  }
}

Nous utilisons des conditions Flutter pour pouvoir personnaliser l'affichage des messages en fonction de l'expéditeur ou du lecteur.

Dans le cas d'un message de l'autre utilisateur, la ligne sera constituée de sa photo de profil que nous mettons dans un Container circulaire.

Nous plaçons ensuite le message texte de manière brut, avec à la suite une icône de lecture suivie de la date d'envoi.

3.6 Création du widget AudioMessage

On continue avec le deuxième type qui permettra d'afficher des messages audio:

Son constructeur va prendre cette fois-ci deux paramètres qui seront fourni à son widget:

  • date: pour l'heure d'envoi du message
  • senderProfile: pour le chemin d'accès à la photo de profil de l'expéditeur

Je passe rapidement sur le contenu assez simple de ce AudioMessage, donc le code complet est le suivant:

class AudioMessage extends StatelessWidget {
  final String date, senderProfile;

  const AudioMessage({
    Key? key,
    required this.date,
    required this.senderProfile,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        SizedBox(
          width: 60,
          child: Row(
            children: [
              Text(
                "17:14",
                style: GoogleFonts.inter(
                  color: dGreen,
                  fontSize: 12,
                  fontWeight: FontWeight.w500,
                ),
              ),
              const SizedBox(width: 7.0),
              const Icon(
                Icons.check,
                color: dGreen,
                size: 13.0,
              ),
            ],
          ),
        ),
        Expanded(
          child: Container(
            alignment: Alignment.centerLeft,
            margin: const EdgeInsets.only(
              left: 15,
              bottom: 5,
            ),
            padding: const EdgeInsets.all(6),
            height: 55,
            decoration: const BoxDecoration(
              color: dGreen,
              borderRadius: BorderRadius.only(
                topLeft: Radius.circular(15),
                topRight: Radius.circular(15),
                bottomRight: Radius.circular(15),
              ),
            ),
            child: Row(
              children: [
                const IconButton(
                  icon: Icon(
                    Icons.play_circle_outline,
                    color: Colors.white,
                  ),
                  onPressed: null,
                ),
                Image.asset(
                  'images/sound-waves.png',
                  height: 35,
                  width: 130,
                  fit: BoxFit.fill,
                ),
              ],
            ),
          ),
        ),
        Container(
          margin: const EdgeInsets.only(
            left: 16,
            right: 10,
          ),
          width: 45,
          height: 45,
          decoration: BoxDecoration(
            color: Colors.white,
            shape: BoxShape.circle,
            image: DecorationImage(
              image: AssetImage(senderProfile),
              fit: BoxFit.cover,
            ),
          ),
        ),
      ],
    );
  }
}

Ici nous retournons aussi un Widget Row avec les mêmes caractéristiques que pour les messages de type texte créés plus haut.

Sauf qu'ici on n'affiche pas de texte, mais une icône permettant de lancer la lecture de l'audio et un visuel de notre message audio.

3.7 Création du widget ImageMessage

On termine avec le dernier type qui permettra d'afficher un message comportant une image et un texte en description:

Son constructeur va prendre trois paramètres qui seront fourni à son widget:

  • date: pour l'heure d'envoi du message
  • senderProfile: pour le chemin d'accès à la photo de profil de l'expéditeur
  • description: qui sera le message text en complément de l'image envoyée

Au niveau du design de ce widget, rien d'exceptionnel, simplement un ensemble de Row et de Column pour aligner sur notre texte en description.

Le tout accompagné de la photo de profil de l'expéditeur comme pour les autres messages.

Je vous laisse donc le code complet du widget ImageMessage:

class ImageMessage extends StatelessWidget {
  final String image, date, description;

  const ImageMessage({
    Key? key,
    required this.image,
    required this.date,
    required this.description,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Container(
          margin: const EdgeInsets.only(
            right: 16,
          ),
          width: 45,
          height: 45,
        ),
        Expanded(
          child: Column(
            children: [
              Container(
                margin: const EdgeInsets.only(
                  right: 26,
                  top: 5,
                ),
                height: 150,
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: AssetImage(image),
                    fit: BoxFit.cover,
                  ),
                  borderRadius: const BorderRadius.all(
                    Radius.circular(22.0),
                  ),
                ),
              ),
              Container(
                margin: const EdgeInsets.only(
                  top: 8,
                  right: 25,
                  bottom: 10,
                ),
                padding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
                height: 55,
                decoration: const BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.all(Radius.circular(12)),
                ),
                child: Wrap(children: [
                  Text(
                    description,
                    textAlign: TextAlign.center,
                    style: GoogleFonts.inter(
                      color: dGreen,
                      fontSize: 12,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ]),
              ),
            ],
          ),
        ),
        SizedBox(
          width: 60,
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              const Icon(
                Icons.check,
                color: dGreen,
                size: 13.0,
              ),
              const SizedBox(width: 7.0),
              Text(
                "17:14",
                style: GoogleFonts.inter(
                  color: dGreen,
                  fontSize: 12,
                  fontWeight: FontWeight.w500,
                ),
              ),
            ],
          ),
        )
      ],
    );
  }
}

Tuto Flutter WhatsApp | Conclusion

Nous sommes donc rendus au terme de ce tuto Flutter WhatsApp dans lequel nous avons abordé de nombreuses notions de design.

C’est le quatrième d’une longue série qui vient compléter la formation Flutter Révolution avec du contenu plus orienté sur le design pratique avec Flutter.

C’est aussi l’occasion pour un plus grand nombre de personnes de découvrir Flutter et de tester en direct son fonctionnement.

Si ce n’est pas déjà fait, commencez le cours gratuit Flutter pour installer Flutter et le configurer sur votre ordinateur.

Rejoignez le cours complet Flutter Révolution si vous souhaitez créer des applications complètes avec Firebase, Google Maps et Stripe.