Bonjour à tous et bienvenue dans ce nouveau tutorial Flutter en français sur la création d’une application mobile de réservation d’hôtel.

Il s’agit d’un tutorial Flutter en français qui vous permettra de découvrir et de manipuler de nombreux concepts du langage Dart et du Framework Flutter.

Nous allons créer pas à pas le design de cette application avec Flutter et reproduire ses deux principales pages.

La première page sera notre page d’accueil dans laquelle nous pourrons consulter les différents hôtels, tandis que la deuxième la page de sélection des dates:

Nous allons donc séparer ce tutorial Flutter en français en trois parties:

  1. Création et configuration de l’application
  2. Design de la page d’accueil de notre application
  3. Création de la page de réservation

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

Flutter Tutorial Français | 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 que nous utiliserons par la suite.

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.

Nous nous positionnons dans notre dossier de développement et nous ouvrons ensuite le terminal pour lancer la commande:

flutter create hotels

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 des hôtels

Notre application aura besoin des photos des hôtels mises en avant dans notre application, nous devons donc ajouter ces images dans le projet.

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

Ce dossier contiendra donc quatre images que nous utiliserons dans notre application.

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

assets:
  - images/hotel_1.png
  - images/hotel_2.png
  - images/hotel_3.png
  - images/hotel_4.png

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 tutorial Flutter en français, nous allons utiliser le package google_fonts qui donne accès à des centaines de polices : 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:

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  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

À la première ligne de notre fichier main.dart, nous allons importer le package Material qui nous donnera accès à tous les widgets et autres outils que Flutter propose.

Ensuite, nous pouvons importer le deuxième package Google Fonts pour accéder à toutes les polices personnalisées.

Notre fichier main.dart ressemble maintenant à ceci :

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: 'Hotels Booking',
      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 une variable de couleur nommé d_green qui va contenir un code couleur précis.

const d_green = const Color(0xFF54D3C2);

Nous utiliserons cette couleur dans toute notre application, que nous définissons ici en utilisant un code hexadécimal.

Flutter Tutorial Français | 2. Création de la page d’accueil

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

2.1. Architecture de la HomePage

Afin de rendre notre code plus lisible, nous alors diviser notre page en plusieurs widgets qui seront créés indépendamment avant d’être appelé 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('Home'),
      ),
      body: Container(),
    );
  }
}

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

Il nous permettra de définir notre appBar, notre body et ainsi qu’une bottomNavigationBar.

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

Notre appBar est constitué de trois icônes que nous allons insérer en utilisant ici le widget IconButton et d’un texte que nous allons ajouter en utilisant le widget Text.

Dans la déclaration de notre appBar, nous allons aussi renseigner les propriétés nécessaires à notre widget AppBar:

  • la propriété backgroundColor avec pour valeur une couleur blanche
  • la propriété centerTitle pour centrer notre titre

Pour le reste, cela reste classique, on place notre première icône dans le champ leading grâce au widget IconButton.

Ensuite, on utilise le widget Text pour le champ title et pour la deux autres icônes, nous les définissons dans le champ actions comme vous pouvez le voir dans le code ci-dessous :

class MyAppBar extends StatelessWidget implements PreferredSizeWidget {
  Size get preferredSize => new Size.fromHeight(50);
  @override
  Widget build(BuildContext context) {
    return AppBar(
      leading: IconButton(
        icon: Icon(
          Icons.arrow_back,
          color: Colors.grey[800],
          size: 20,
        ),
        onPressed: null,
      ),
      centerTitle: true,
      title: Text(
        'Explore',
        style: GoogleFonts.nunito(
          color: Colors.black,
          fontSize: 22,
          fontWeight: FontWeight.w800,
        ),
      ),
      actions: [
        IconButton(
          icon: Icon(
            Icons.favorite_outline_rounded,
            color: Colors.grey[800],
            size: 20,
          ),
          onPressed: null,
        ),
        IconButton(
          icon: Icon(
            Icons.place,
            color: Colors.grey[800],
            size: 20,
          ),
          onPressed: null,
        ),
      ],
      backgroundColor: Colors.white,
    );
  }

Sans oublier de donner une taille et une couleur à nos icônes pour les rendre plus conforme au design original.

Placez ensuite le widget de notre AppBar dans le champ dédié de notre Scaffold:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: MyAppBar(),
    );
  }
}

2.3 Création de la SearchSection de notre page d’accueil

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

La première section (SearchSection) sera constituée de deux row:

  • La première contiendra un champ de texte et un bouton pour lancer la recherche
  • La deuxième contiendra les dates et chambres choisies pour la réservation

La deuxième section de notre page (HotelSection) quant à elle est constituée du résultat de la recherche, notamment une image de l’hôtel et ses informations complémentaire.

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

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: MyAppBar(),
      body: SingleChildScrollView(
        child: Column(
          children: [
            SearchSection(),
            HotelSection(),
          ],
        ),
      ),
    );
  }
}

Le corps de notre page d’accueil est contenu dans le widget principal SingleChildScrollView et dans une Colum pour permettre de scroller à la verticale.

Le premier enfant de notre SearchSection est donc une Row qui elle va contenir le champ de texte et le bouton de recherche.

Nous mettons le champ texte dans le widget Expanded afin qu’il remplisse l'espace disponible le long de l'axe principal du widget, ceci pour gérer le responsive de notre page.

Pour le deuxième enfant de notre SearchSection, nous avons une autre Row comportant des widgets Text classiques comme vous pouvez le voir dans le code complet:

class SearchSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.grey[200],
      padding: EdgeInsets.fromLTRB(10, 25, 10, 10),
      child: Column(
        children: [
          Row(
            children: [
              Expanded(
                child: Container(
                  padding: EdgeInsets.only(left: 5),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(30),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.grey.shade300,
                        blurRadius: 4,
                        offset: Offset(0, 3),
                      ),
                    ],
                  ),
                  child: TextField(
                    decoration: InputDecoration(
                      hintText: 'London',
                      contentPadding: EdgeInsets.all(10),
                      border: InputBorder.none,
                    ),
                  ),
                ),
              ),
              SizedBox(width: 10),
              Container(
                height: 50,
                width: 50,
                decoration: BoxDecoration(
                  boxShadow: [
                    BoxShadow(
                      color: Colors.grey.shade300,
                      blurRadius: 4,
                      offset: Offset(0, 4),
                    ),
                  ],
                  borderRadius: BorderRadius.all(
                    Radius.circular(25),
                  ),
                ),
                child: ElevatedButton(
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) {
                          return CalendarPage();
                        },
                      ),
                    );
                  },
                  child: Icon(
                    Icons.search,
                    color: Colors.white,
                    size: 26,
                  ),
                  style: ElevatedButton.styleFrom(
                    padding: EdgeInsets.all(10),
                    shape: CircleBorder(),
                    shadowColor: Colors.white,
                    primary: d_green,
                  ),
                ),
              )
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Container(
                margin: EdgeInsets.all(10),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Choose date',
                      style: GoogleFonts.nunito(
                        color: Colors.grey,
                        fontSize: 15,
                      ),
                    ),
                    SizedBox(height: 8),
                    Text(
                      '12 Dec - 22 Dec',
                      style: GoogleFonts.nunito(
                        color: Colors.black,
                        fontSize: 17,
                      ),
                    ),
                  ],
                ),
              ),
              Container(
                margin: EdgeInsets.all(10),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Number of Rooms',
                      style: GoogleFonts.nunito(
                        color: Colors.grey,
                        fontSize: 15,
                      ),
                    ),
                    SizedBox(height: 8),
                    Text(
                      '1 Room - 2 Adults',
                      style: GoogleFonts.nunito(
                        color: Colors.black,
                        fontSize: 17,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          )
        ],
      ),
    );
  }
}

Pour permettre la navigation de la page d’accueil vers la page de réservation qui sera créée dans la deuxième partie, nous ajoutons la propriété onPress à notre MaterialButton:

onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) {
        return CalendarPage();
      },
    ),
  );
},

Vous devrez bien sûr déclarer votre page calendar_page.dart pour faire fonctionner votre code et ainsi activer la navigation.

2.4 Création de la HotelSection de notre page d’accueil

Comme deuxième enfant de la colonne de notre body, nous avons le widget HotelSection que nous allons créer.

Ce dernier nous permet d’afficher les informations des différents hôtels trouvés lors de la recherche, je vous laisse le code final pour y jeter un oeil:

class HotelSection extends StatelessWidget {
  final List hotelList = [
    {
      'title': 'Grand Royl Hotel',
      'place': 'wembley, London',
      'distance': 2,
      'review': 36,
      'picture': 'images/hotel_1.png',
      'price': '180',
    },
    {
      'title': 'Queen Hotel',
      'place': 'wembley, London',
      'distance': 3,
      'review': 13,
      'picture': 'images/hotel_2.png',
      'price': '220',
    },
    {
      'title': 'Grand Mal Hotel',
      'place': 'wembley, London',
      'distance': 6,
      'review': 88,
      'picture': 'images/hotel_3.png',
      'price': '400',
    },
    {
      'title': 'Hilton',
      'place': 'wembley, London',
      'distance': 11,
      'review': 34,
      'picture': 'images/hotel_4.png',
      'price': '910',
    },
  ];
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(10),
      color: Colors.white,
      child: Column(
        children: [
          Container(
            height: 50,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  '550 hotels founds',
                  style: GoogleFonts.nunito(
                    color: Colors.black,
                    fontSize: 15,
                  ),
                ),
                Row(
                  children: [
                    Text(
                      'Filters',
                      style: GoogleFonts.nunito(
                        color: Colors.black,
                        fontSize: 15,
                      ),
                    ),
                    IconButton(
                      icon: Icon(
                        Icons.filter_list_outlined,
                        color: d_green,
                        size: 25,
                      ),
                      onPressed: null,
                    ),
                  ],
                )
              ],
            ),
          ),
          Column(
            children: hotelList.map((hotel) {
              return HotelCard(hotel);
            }).toList(),
          ),
        ],
      ),
    );
  }
}

Dans un premier temps nous créons une variable locale pour stocker toute la liste des hôtels ainsi que leurs informations:

final List hotelList = [
    {
      'title': 'Grand Royl Hotel',
      'place': 'wembley, London',
      'distance': 2,
      'review': 80,
      'picture': 'images/hotel1.jpg',
      'price': '180',
    },
    {
      'title': 'Queen Hotel',
      'place': 'wembley, London',
      'distance': 2,
      'review': 80,
      'picture': 'images/hotel2.jpg',
      'price': '220',
    },
    [...]
  ];

Par la suite nous parcourons notre liste de données pour retourner à chaque fois le même widget, mais avec des données différentes.

Dans l’exemple suivant, nous montrons comment nous parcourons notre liste et nous affichons les noms d’hôtels par exemple:

Column(
  children: hotelList.map((hotel) {
    return Text(hotel['title']);
  }).toList(),
)

On crée ensuite le widget HotelCard de manière séparée pour créer le design de notre carte d'hôtel de manière précise.

Ce widget prend en paramètre un objet Map pour remplir son contenu, avec une image et différentes informations comme le nom et le prix de l'hôtel.

class HotelCard extends StatelessWidget {
  final Map hotelData;
  HotelCard(this.hotelData);
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(10),
      height: 230,
      width: double.infinity,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.all(
          Radius.circular(18),
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.shade200,
            spreadRadius: 4,
            blurRadius: 6,
            offset: Offset(0, 3),
          ),
        ],
      ),
      child: Column(
        children: [
          Container(
            height: 140,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.only(
                topLeft: Radius.circular(18),
                topRight: Radius.circular(18),
              ),
              image: DecorationImage(
                image: AssetImage(
                  hotelData['picture'],
                ),
                fit: BoxFit.cover,
              ),
            ),
            child: Stack(
              children: [
                Positioned(
                  top: 5,
                  right: -15,
                  child: MaterialButton(
                    color: Colors.white,
                    shape: CircleBorder(),
                    onPressed: () {},
                    child: Icon(
                      Icons.favorite_outline_rounded,
                      color: d_green,
                      size: 20,
                    ),
                  ),
                )
              ],
            ),
          ),
          Container(
            margin: EdgeInsets.fromLTRB(10, 10, 10, 0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  hotelData['title'],
                  style: GoogleFonts.nunito(
                    fontSize: 18,
                    fontWeight: FontWeight.w800,
                  ),
                ),
                Text(
                  '\$' + hotelData['price'],
                  style: GoogleFonts.nunito(
                    fontSize: 18,
                    fontWeight: FontWeight.w800,
                  ),
                ),
              ],
            ),
          ),
          Container(
            margin: EdgeInsets.symmetric(horizontal: 10),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  hotelData['place'],
                  style: GoogleFonts.nunito(
                    fontSize: 14,
                    color: Colors.grey[500],
                    fontWeight: FontWeight.w400,
                  ),
                ),
                Row(
                  children: [
                    Icon(
                      Icons.place,
                      color: d_green,
                      size: 14.0,
                    ),
                    Text(
                      hotelData['distance'].toString() + ' km to city',
                      style: GoogleFonts.nunito(
                        fontSize: 14,
                        color: Colors.grey[500],
                        fontWeight: FontWeight.w400,
                      ),
                    ),
                  ],
                ),
                Text(
                  'per night',
                  style: GoogleFonts.nunito(
                    fontSize: 14,
                    color: Colors.grey.shade800,
                    fontWeight: FontWeight.w400,
                  ),
                ),
              ],
            ),
          ),
          Container(
            margin: EdgeInsets.fromLTRB(10, 3, 10, 0),
            child: Row(
              children: [
                Row(
                  children: [
                    Icon(
                      Icons.star_rate,
                      color: d_green,
                      size: 14.0,
                    ),
                    Icon(
                      Icons.star_rate,
                      color: d_green,
                      size: 14.0,
                    ),
                    Icon(
                      Icons.star_rate,
                      color: d_green,
                      size: 14.0,
                    ),
                    Icon(
                      Icons.star_rate,
                      color: d_green,
                      size: 14.0,
                    ),
                    Icon(
                      Icons.star_border,
                      color: d_green,
                      size: 14.0,
                    ),
                  ],
                ),
                SizedBox(width: 20),
                Text(
                  hotelData['review'].toString() + ' reviews',
                  style: GoogleFonts.nunito(
                    fontSize: 14,
                    color: Colors.grey[500],
                    fontWeight: FontWeight.w400,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Sur chaque photo d’hôtel, nous avons un bouton like comportant une icône pour mettre en favoris l’hôtel en question.

Pour réaliser cela nous utilisons un widget Stack avec un widget Positioned pour positionner le bouton en haut à droite de sur l’image.

Stack(
  children: [
    Positioned(
      top: 5,
      right: -15,
      child: MaterialButton(
        color: Colors.white,
        shape: CircleBorder(),
        onPressed: () {},
        child: Icon(
          Icons.favorite_outline_rounded,
          color: d_green,
          size: 20,
        ),
      ),
    )
  ],
),

2.5 Création de notre bottomNavigationBar

Pour créer notre barre de navigation, nous utilisons le widget classique BottomNavigationBar().

Notre barre de navigation est constitué de trois icônes et labels, voilà donc son code assez classique:

class BottomNavBarSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      selectedItemColor: Colors.grey[600],
      items: [
        BottomNavigationBarItem(
          icon: Icon(
            Icons.search,
            color: d_green,
          ),
          label: 'Explore',
        ),
        BottomNavigationBarItem(
          icon: Icon(
            Icons.favorite_outline_rounded,
            color: d_green,
          ),
          label: 'Tips',
        ),
        BottomNavigationBarItem(
          icon: Icon(
            Icons.person,
            color: d_green,
          ),
          label: 'Profile',
        ),
      ],
    );
  }
}

La méthode build de notre classe BottomNavBarSection renvoie donc le widget BottomNavigationBar.

Le seul attribut que nous mentionnons est celui de selectedItemColor pour nous permettre de définir la couleur lors de la sélection de l’item de notre barre.

Les différents items de notre barre de navigation sont créés avec le widget BottomNavigationBarItem et contient donc chacun une icône et un label.

Nous avons donc terminer avec notre page d’accueil, passons maintenant à la création de la page de sélection de dates.

Flutter Tutorial Français | 3. Création de la page de calendrier

Dans cette partie de ce tutorial Flutter en français, nous allons nous concentrer sur l’intégration du design de notre page de calendrier, que nous divisons en trois sections:

  • La première section constituée des périodes sélectionnés sur le calendrier
  • La deuxième section sera constituée du calendrier ou nous allons sélectionner nos dates de réservation
  • La troisième section comportera le bouton d’envoi pour appliquer les dates sélectionner sur le calendrier.

3.1 Création du fichier de la page calendrier

Comme nous l’avons dit au début de ce tutorial Flutter en français, nous allons mettre le contenu de la page de réservation dans un fichier différent, ceci afin de bien organiser notre code et le rendre plus lisible.

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

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

import 'calendar_page.dart';
// ou
import 'package:hotels/calendar_page.dart';

Une fois notre page créé, nous commençons par importé le package principal que nous utiliserons, notamment le Material comme le montre le code suivant :

import 'package:flutter/material.dart';

3.2 Installation du package table_calendar

À la deuxième ligne de notre fichier calendar_page.dart, nous allons importer le package table_calendar qui nous donne accès au Widget de calendrier hautement personnalisable et riche en fonctionnalités :

import 'package:table_calendar/table_calendar.dart';

Le package table_calendar nous permettra d’intégrer rapidement un calendrier dans notre application et de le personnaliser à notre guise, afin de permettre aux utilisateurs de sélectionner des dates.

Bien sûr qu’il faudra d'abords l’installer avant de l’importer dans notre page:
https://pub.dev/packages/table_calendar

Pour cela nous devons ajouter la dépendance du package table_calendar dans le fichier pubspec.yaml, qui ressemblera à ceci:

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  google_fonts: ^2.1.0
  table_calendar: ^3.0.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

Nos deux packages étant importés, nous allons commencer à construire notre page de réservation pas à pas.

Nous déclarons notre classe principale dans laquelle se trouvera tout notre code de la page de réservation, la classe CalendarPage():

class CalendarPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: MyAppBar(),
      body: Container(),
    );
  }
}

3.3 Création de la appBar

Notre appBar est presque vide, elle ne propose qu'un bouton retour personnalisé, pour le colorer en gris et permettre de revenir sur la page d'accueil.

Voilà le code de notre appBar et de son bouton retour:

class MyAppBar extends StatelessWidget implements PreferredSizeWidget {
  Size get preferredSize => new Size.fromHeight(60);
  @override
  Widget build(BuildContext context) {
    return AppBar(
      leading: IconButton(
        icon: Icon(
          Icons.arrow_back,
          color: Colors.grey[800],
          size: 20,
        ),
        onPressed: () {
          Navigator.pop(context);
        },
      ),
      backgroundColor: Colors.white,
    );
  }
}

3.4 Création du body de notre application

Maintenant, nous passons au corps de notre page, qui n’utilisera pas cette fois-ci de SingleChildScrollView(), car on n’aura pas besoin de scroller le contenu.

Ici, nous plaçons directement le corps de notre page dans une Column dans laquelle nous mettrons tous les widgets qui vont constituer notre corps.

Voilà donc le code complet de notre body ci-dessous :

body: Column(
  children: [
    PeriodSection(),
    CalendarRange(),
    ValidateBookingSection(),
  ],
),

Notre body comme mentionné au tout début comporte trois sections et pour bien organiser cela, nous créons nos propres widgets que nous appelons par la suite comme des enfants de notre widget Column.

Mais avant la création de nos trois sections, nous déclarons les variables qui serons utilisées plus bas :

DateTime kNow = DateTime.now();
DateTime kFirstDay = DateTime(kNow.year, kNow.month - 3, kNow.day);
DateTime kLastDay = DateTime(kNow.year, kNow.month + 3, kNow.day);

const d_green = const Color(0xFF54D3C2);

Voilà les différentes variables et leur utilité:

  • kNow: représente la date du jour
  • kFirstDay: stocke le premier jour disponible sur notre calendrier.
  • kLastDay: stock le dernier jour disponible sur notre calendrier.
  • d_green: notre couleur verte principale utilisée

3.5 Création de la première section PeriodSection

Nous déclarons donc le widget PeriodSection qui est par la suite appelé comme vous pouvez le voir dans le code du body plus haut.

class PeriodSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(
                  'Depart',
                  textAlign: TextAlign.left,
                  style: TextStyle(
                    fontWeight: FontWeight.w400,
                    fontSize: 16,
                    color: Colors.grey[700],
                  ),
                ),
                SizedBox(
                  height: 4,
                ),
                Text(
                  'Month 12 Dec',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
              ],
            ),
            Container(
              height: 60,
              width: 1,
              color: Colors.grey[350],
            ),
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(
                  'Return',
                  style: TextStyle(
                    fontWeight: FontWeight.w400,
                    fontSize: 16,
                    color: Colors.grey[700],
                  ),
                ),
                SizedBox(
                  height: 4,
                ),
                Text(
                  'Tues 22 Dec',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
              ],
            ),
          ],
        ),
        Divider(
          color: Colors.grey,
          height: 1,
        ),
      ],
    );
  }
}

Ici nous déclarons directement une Column qui lui a plusieurs enfants, des widgets Text auxquels nous appliquons ensuite différentes propriétés de style comme vous pouvez voir dans le code.

Pour les traits de séparation, nous utilisons pour le premier un widget Container vide et avec une hauteur et largeur fixe.

Ensuite nous lui donnons une couleur grise :

Container(
   height: 60,
   width: 1,
   color: Colors.grey[350],
),

Pour le deuxième trait de séparation horizontale, nous utilisons le widget Divider:

Divider(
  color: Colors.grey,
  height: 1,
 ),

3.6 Création de la deuxième section CalendarRange

C’est dans cette section que nous allons utiliser notre package table_calendar télécharger au tout début.

Nous allons donc créer ici un StatefulWidget, car le contenu de note section sera modifié en fonction des actions de l’utilisateur, notamment lors de la sélection des jours sur le calendrier:

class CalendarRange extends StatefulWidget {
  @override
  _CalendarRangeState createState() => _CalendarRangeState();
}

Par la suite, nous implémentons donc la classe _CalendarRangeState qui va retourner un Container avec une marge intérieure dans lequel sera notre calendrier:

class _CalendarRangeState extends State<CalendarRange> {
  CalendarFormat _calendarFormat = CalendarFormat.month;
  RangeSelectionMode _rangeSelectionMode = RangeSelectionMode
      .toggledOn; // Can be toggled on/off by longpressing a date
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;
  DateTime? _rangeStart;
  DateTime? _rangeEnd;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(15),
      color: Colors.grey[10],
      child: Expanded(
        child: TableCalendar(
          lastDay: kLastDay,
          focusedDay: _focusedDay,
          selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
          rangeStartDay: _rangeStart,
          rangeEndDay: _rangeEnd,
          calendarFormat: _calendarFormat,
          headerStyle: HeaderStyle(
            titleCentered: true,
            formatButtonVisible: false,
            titleTextStyle: TextStyle(fontSize: 18),
          ),
          calendarStyle: CalendarStyle(
            isTodayHighlighted: false,
            rangeHighlightColor: d_green,
            rangeStartDecoration: BoxDecoration(
              color: d_green,
              shape: BoxShape.circle,
              border: Border.fromBorderSide(
                BorderSide(
                  color: Colors.white,
                  width: 3,
                  style: BorderStyle.solid,
                ),
              ),
            ),
            withinRangeTextStyle: TextStyle(
              color: Colors.white,
            ),
            rangeEndDecoration: BoxDecoration(
              color: d_green,
              shape: BoxShape.circle,
              border: Border.fromBorderSide(
                BorderSide(
                  color: Colors.white,
                  width: 3,
                  style: BorderStyle.solid,
                ),
              ),
            ),
          ),
          firstDay: kFirstDay,
          rangeSelectionMode: _rangeSelectionMode,
          onDaySelected: (selectedDay, focusedDay) {
            if (!isSameDay(_selectedDay, selectedDay)) {
              setState(() {
                _selectedDay = selectedDay;
                _focusedDay = focusedDay;
                _rangeStart = null;
                _rangeEnd = null;
                _rangeSelectionMode = RangeSelectionMode.toggledOff;
              });
            }
          },
          onRangeSelected: (start, end, focusedDay) {
            setState(() {
              _selectedDay = null;
              _focusedDay = focusedDay;
              _rangeStart = start;
              _rangeEnd = end;
              _rangeSelectionMode = RangeSelectionMode.toggledOn;
            });
          },
          onFormatChanged: (format) {
            if (_calendarFormat != format) {
              setState(() {
                _calendarFormat = format;
              });
            }
          },
          onPageChanged: (focusedDay) {
            _focusedDay = focusedDay;
          },
        ),
      ),
    );
  }
}

Dans les premières lignes de notre classe, nous déclarons les variables nous permettant d’initialiser le widget TablebleCalendar du package table_calendar.

 CalendarFormat _calendarFormat = CalendarFormat.month;
  RangeSelectionMode _rangeSelectionMode = RangeSelectionMode
      .toggledOn;
  DateTime _focusedDay = DateTime.now();
  DateTime _selectedDay;
  DateTime _rangeStart;
  DateTime _rangeEnd;

Pour en savoir plus vous pouvez, vous rendre dans la documentation du package: https://pub.dev/documentation/table_calendar/latest/

Pour personnaliser l’entête du calendrier et le centrer, nous redéfinissons la propriété headerStyle du widget TableCalendar:

headerStyle: HeaderStyle(
  titleCentered: true,
  formatButtonVisible: false,
  titleTextStyle: TextStyle(fontSize: 18),
),

Pour les autres attributs que nous avons redéfinis, vous pouvez retrouver tous les détails dans la documentation du package utilisé.

3.7 Création de la dernière section ValidateBookingSection

Cette section n’est constitué que de deux principaux widgets, une RadioListTile et un ElevatedButton.

Nous stylons ensuite nos widgets pour avoir un rendu semblable au design original, voilà donc le code complet de cette section :

class ValidateBookingSection extends StatelessWidget {
  final selectedRadio = 1;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RadioListTile(
          value: selectedRadio,
          activeColor: d_green,
          groupValue: selectedRadio,
          selected: true,
          title: Text(
            "Flexible with dates",
            style: new TextStyle(
              color: Colors.black,
              fontSize: 20.0,
            ),
          ),
          onChanged: null,
        ),
        Container(
          padding: EdgeInsets.symmetric(horizontal: 20),
          width: double.infinity,
          child: ElevatedButton(
            child: Text(
              'Apply',
              style: TextStyle(fontSize: 17),
            ),
            style: ElevatedButton.styleFrom(
              primary: d_green,
              padding: EdgeInsets.all(15),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(30.0),
              ),
            ),
            onPressed: () {
              print('Apply Booking');
              Navigator.pop(context);
            },
          ),
        ),
      ],
    );
  }
}

Flutter Tutorial Français | Conclusion

Nous sommes donc rendus au terme de ce tutorial Flutter en français sur le blog.

C’est le deuxième d’une longue série qui viendra 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.