Bonjour à tous et bienvenue dans ce nouveau tuto Flutter consacré au modèle de conception BLoC.

C’est un tuto Flutter destiné aux débutants qui souhaitent découvrir BLoC, son fonctionnement et son utilité dans Flutter.

BLoC (Business Logic Components) est un modèle de conception spécifiquement créé pour Flutter et qui permet de gérer le « state management » de nos apps.

Concrètement, BLoC nous permet de traiter le contenu dynamique de nos applications avec une architecture dédiée.

Nous allons prendre aujourd’hui un exemple complémentaire aux tutos de la Flutter Académie dédiée à l’apprentissage de BloC.

Il s’agit de créer la fonctionnalité de changement de thème couleur en utilisant le modèle Flutter BLoC:

Tuto Flutter BLoC Débutant

Pour réaliser cette application, nous allons suivre les étapes suivantes:

  1. Les bases de l’application
  2. Création de notre ThemeCubit
  3. Création de la vue avec la ThemePage
  4. Création de la page de démo

Vous pouvez également télécharger le code source de l’application pour la tester plus rapidement.

1. Les bases de l'application

On commence avec une première partie dédiée aux bases de l'application et notamment aux fichiers racines de notre projet.

Ce tuto Flutter BloC est destiné aux débutants et a pour objectif de vous proposer un exemple simple de projet BLoC.

La première chose à comprendre concernant le modèle BLoC, est la structure de notre projet, que vous allez pouvoir reprendre.

Je vous propose l'organisation suivante pour séparer notre code en différents dossiers et fichiers:

├── lib
│   ├── app.dart
│   ├── theme
│   │   ├── theme.dart
│   │   ├── cubit
│   │   │   └── theme_cubit.dart
│   │   └── view
│   │       ├── demo_page.dart
│   │       ├── theme_page.dart
│   │       └── theme_view.dart
│   ├── theme_observer.dart
│   └── main.dart
├── pubspec.lock
├── pubspec.yaml

La principale chose à retenir dans le modèle BLoC, est que nous séparons la logique métier de la vue de notre application.

Concrètement, on sépare la logique dynamique de notre application de son affichage, dans notre cas tout ce qui concerne la gestion du thème.

La fonction de changement de thème sera donc déclarée dans notre logique métier (ici dans un Cubit) et l'affichage dans un fichier dédié.

Vous comprendrez toutes ces notions au fur et à mesure de l'avancement de l'application et du tuto.

On continue en regardant du côté des packages BLoC qui sont indispensables pour accéder aux widgets Bloc et Cubit.

Vous aurez besoin des deux packages suivants pour réaliser la totalité de notre application:

Vous pouvez également ajouter la dépendance de ces deux packages dans votre fichier YAML:

flutter_bloc: ^8.0.1
bloc: ^8.0.3

On commence enfin la rédaction de nos fichiers Dart, pour cette partie qui se situent à la racine de notre dossier "lib/".

Le premier fichier étant naturellement le point d'entrée main.dart avec le premier code de type BLoC:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'theme_observer.dart';
import 'app.dart';

void main() {
  BlocOverrides.runZoned(
    () => runApp(const ThemeApp()),
    blocObserver: ThemeBlocObserver(),
  );
}

Ce premier code nous permet de connecter notre application ThemeApp() au widget d'observation ThemeBlocObserver().

Un widget d'observation de type BlocObserver permet de repérer les changements d'états de notre application et de récupérer des informations.

On sera par exemple capable de repérer et d'afficher dans la console chaque changement de thème couleur de notre app.

Voilà donc le code de mon widget ThemeBlocObserver() dans sa version la plus simple:

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

class ThemeBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    if (bloc is Cubit) print(change.toString());
  }
}

On manipule ici la méthode onChange() qui va donc renvoyer les changements d'états dans la console.

On peut également repérer les erreurs ou les transitions d'états dans des exemples plus avancés.

Pour le moment, cette simple fonction onChange() nous suffira largement pour repérer le changement de thème couleur.

On passe ensuite à la création du widget ThemeApp() qui représente toute notre application Flutter:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'theme/theme.dart';

class ThemeApp extends StatelessWidget {
  const ThemeApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ThemeCubit(),
      child: const ThemePage(),
    );
  }
}

Je ne déclare pas encore mon widget MaterialApp() car j'aurais besoin de sa propriété de thème pour le rendre dynamique.

Avant cela, je dois associer mon application avec la logique métier de notre gestionnaire de thème ThemeCubit().

Le widget ThemePage() contient le code de notre page avec sa vue et le widget ThemeCubit() la méthode de changement de thème.

Nos deux éléments sont donc connectés, il nous faut maintenant les déclarer pour faire fonctionner le tout.

Cours Flutter Gratuit

2. Création de notre ThemeCubit

On commence avec la création de notre logique métier, et dans notre cas du Cubit dédié à la gestion du thème couleur.

Lorsqu'on manipule un Bloc ou un Cubit, on va émettre des états pour modifier le contenu de notre application.

Dans notre fonctionnalité, le nouvel état sera un nouveau thème couleur de type ThemeData().

Par exemple, on peut proposer le thème couleur par défaut dans la variable _defaultTheme:

static final _defaultTheme = ThemeData(
  primarySwatch: Colors.blue,
  appBarTheme: const AppBarTheme(color: Colors.blue),
  brightness: Brightness.light,
);

On peut ainsi initialiser notre Cubit avec cette variable et la syntaxe suivante:

ThemeCubit() : super(_defaultTheme);

Il ne nous reste plus qu'à créer la méthode de changement de thème qui prendra en paramètre la couleur et la luminosité.

On appelle cette méthode changeTheme() et elle prend en paramètres des variables de type MaterialColor et Brightness:

void changeTheme(MaterialColor themeColor, Brightness themeBrightness) {
  final colorTheme = ThemeData(
    primarySwatch: themeColor,
    primaryColor: themeColor,
    appBarTheme: AppBarTheme(color: themeColor),
    floatingActionButtonTheme: FloatingActionButtonThemeData(
      backgroundColor: themeColor,
    ),
    brightness: themeBrightness,
  );
  emit(colorTheme);
}

Lors d'un changement de thème, on renvoie en fait un nouveau thème couleur de type ThemeData() avec de nouvelles propriétés.

On modifie par exemple le champ primarySwatch et brightness pour le thème principal.

Vous pouvez également personnaliser le style de certains widgets plus précisément avec les champs appBarTheme ou floatingActionButtonTheme par exemple.

Le code complet de notre ThemeCubit est le suivant:

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

class ThemeCubit extends Cubit<ThemeData> {
  ThemeCubit() : super(_defaultTheme);

  static final _defaultTheme = ThemeData(
    primarySwatch: Colors.blue,
    appBarTheme: const AppBarTheme(color: Colors.blue),
    brightness: Brightness.light,
  );

  void changeTheme(MaterialColor themeColor, Brightness themeBrightness) {
    final colorTheme = ThemeData(
      primarySwatch: themeColor,
      primaryColor: themeColor,
      appBarTheme: AppBarTheme(color: themeColor),
      floatingActionButtonTheme: FloatingActionButtonThemeData(
        backgroundColor: themeColor,
      ),
      brightness: themeBrightness,
    );
    emit(colorTheme);
  }
}

Il s'agit de l'un des exemples les plus simples de Cubit que vous allez pouvoir réaliser avec Flutter Bloc.

L'idée, encore une fois, est que nous manipulons un thème couleur que nous allons émettre dans notre application qui se rechargera.

Le changement sera instantané pour l'utilisateur et s'appliquera alors à toute l'application.

Avant de passer à la suite, vous pouvez exporter votre fichier dans le fichier theme.dart:

export 'cubit/theme_cubit.dart';
export 'view/theme_page.dart';
export 'view/theme_view.dart';
export 'view/demo_page.dart';

Ce fichier permettra de rassembler vos fichiers bloc et view pour n'avoir à importer qu'un seul fichier par la suite.

Tuto Flutter BLoC Débutant

3. Création de la vue avec la ThemePage

On passe enfin à la création de notre vue pour afficher le gestionnaire de thème à l'écran.

Pour cela, on va déclarer le widget ThemePage() qui va renvoyer un BlocBuilder() pour gérer les changements d'états.

Juste après, on renvoie une MaterialApp() avec le champ theme qui va donc être dynamique et se modifier au fil du temps.

Concrètement, le BlocBuilder associé à une MaterialApp va permettre à toute notre application de se recharger en cas de changement de thème:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../theme.dart';

class ThemePage extends StatelessWidget {
  const ThemePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ThemeCubit, ThemeData>(
      builder: (_, theme) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          theme: theme,
          home: ThemeView(),
        );
      },
    );
  }
}

Souvenez-vous donc toujours, que vous avez besoin de manipuler au minimum deux widgets BLoC: BlocProvider d'abord et BlocBuilder ensuite.

Le premier permet de fournir les données issues de notre Cubit et le deuxième de reconstruire la vue à chaque changement d'état.

On passe maintenant à l'affichage et au contenu de notre vue qui se situe dans le widget ThemeView().

On va tout d'abord générer une liste de données pour stocker tous nos thèmes couleurs différents.

On appelle cette variable themeList et elle prend en compte les champs suivants:

final List themeList = [
  {
    'title': 'Violet clair',
    'color': Colors.purple,
    'brightness': Brightness.light,
  },
  {...},
];

On va donc stocker pour chaque thème différent, une couleur principale et la luminosité associée.

On donne également un titre à chaque thème pour pouvoir bien les différencier lors de leur affichage.

Au niveau de l'affichage de notre interface de gestion de thème, on va proposer une grille de sélection des couleurs.

On utilise pour cela le widget GridView.builder en le calibrant avec notre themeList et l'indice index:

GridView.builder(
  padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 200,
    crossAxisSpacing: 10,
    mainAxisSpacing: 10,
    childAspectRatio: 2,
  ),
  itemCount: themeList.length,
  itemBuilder: (context, index) {
    return Card(
      color: themeList[index]['color'],

De cette manière, on affiche une case pour chaque couleur, en alternant avec deux versions de la même couleur, l'une claire et l'autre sombre.

Puisque nous affichons deux cases par ligne, chaque ligne représente une couleur à la fois en deux versions.

Au niveau du contenu de notre case de sélection, on contient le tout dans un widget InkWell() pour la rendre cliquable.

On associe également son clic à notre méthode changeTheme():

InkWell(
  onTap: () => context.read<ThemeCubit>().changeTheme(
    themeList[index]['color'], themeList[index]['brightness']),
  child: [...],
)

Ici, on transmet les informations du thème couleur à notre fonction, mais la syntaxe courte est la suivante:

context.read<ThemeCubit>().changeTheme()

C'est la manière de récupérer les méthodes déclarées dans un Cubit que vous pouvez exécuter depuis n'importe quel endroit de votre application.

Vous devez préciser de quel Cubit ou Bloc il s'agit, puis appeler votre méthode, par exemple ici changeTheme().

Voilà le code final de notre widget ThemeView() avec la déclaration de la liste complète au début:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../theme.dart';

class ThemeView extends StatelessWidget {
  ThemeView({Key? key}) : super(key: key);

  final List themeList = [
    {
      'title': 'Violet clair',
      'color': Colors.purple,
      'brightness': Brightness.light,
    },
    {
      'title': 'Violet sombre',
      'color': Colors.purple,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Rose clair',
      'color': Colors.pink,
      'brightness': Brightness.light,
    },
    {
      'title': 'Rose sombre',
      'color': Colors.pink,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Rouge clair',
      'color': Colors.red,
      'brightness': Brightness.light,
    },
    {
      'title': 'Rouge sombre',
      'color': Colors.red,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Orange clair',
      'color': Colors.orange,
      'brightness': Brightness.light,
    },
    {
      'title': 'Orange sombre',
      'color': Colors.orange,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Jaune clair',
      'color': Colors.amber,
      'brightness': Brightness.light,
    },
    {
      'title': 'Jaune sombre',
      'color': Colors.amber,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Vert clair',
      'color': Colors.green,
      'brightness': Brightness.light,
    },
    {
      'title': 'Vert sombre',
      'color': Colors.green,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Turquoise clair',
      'color': Colors.teal,
      'brightness': Brightness.light,
    },
    {
      'title': 'Turquoise sombre',
      'color': Colors.teal,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Cyan clair',
      'color': Colors.cyan,
      'brightness': Brightness.light,
    },
    {
      'title': 'Cyan sombre',
      'color': Colors.cyan,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Bleu clair',
      'color': Colors.blue,
      'brightness': Brightness.light,
    },
    {
      'title': 'Bleu sombre',
      'color': Colors.blue,
      'brightness': Brightness.dark,
    },
    {
      'title': 'Indigo clair',
      'color': Colors.indigo,
      'brightness': Brightness.light,
    },
    {
      'title': 'Indigo sombre',
      'color': Colors.indigo,
      'brightness': Brightness.dark,
    },
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Bloc Theme')),
      body: GridView.builder(
        padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 200,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
          childAspectRatio: 2,
        ),
        itemCount: themeList.length,
        itemBuilder: (context, index) {
          return Card(
            color: themeList[index]['color'],
            child: InkWell(
              onTap: () => context.read<ThemeCubit>().changeTheme(
                  themeList[index]['color'], themeList[index]['brightness']),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Expanded(child: Container()),
                  Padding(
                    padding: const EdgeInsets.only(left: 10),
                    child: Text(themeList[index]['title']),
                  ),
                  const SizedBox(height: 7),
                  Container(
                    height: 25,
                    color: themeList[index]['brightness'] == Brightness.light
                        ? Colors.white54
                        : Colors.black54,
                  ),
                ],
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => const DemoPage()),
          );
        },
        child: const Icon(
          Icons.open_in_new,
          color: Colors.white,
        ),
      ),
    );
  }
}

De cette manière, vous êtes capables de modifier en direct tout le thème couleur de votre application.

Vous pouvez également créer des thèmes couleurs un peu plus élaborés, avec des combinaisons de couleurs.

N'oubliez pas que vous pouvez personnaliser quasiment tous les widgets Flutter, donc n'hésitez pas à faire de petits tests.

Tuto Flutter BLoC Débutant

4. Création de la page de démo

On termine avec une dernière partie consacrée au test de notre changement de thème couleur.

L'idée est simplement de créer une page de démonstration pour visualiser le changement de thème.

Pour accéder à cette page, que nous appelons DemoPage(), j'ai créé un bouton flottant dans notre page d'accueil:

floatingActionButton: FloatingActionButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const DemoPage()),
    );
  },
  child: const Icon(
    Icons.open_in_new,
    color: Colors.white,
  ),
),

Concernant le contenu de cette page, il s'agit de widgets classiques comme une AppBar, du texte, des images, un bouton et des icônes.

J'ai regroupé le tout dans une colonne et distingué chaque section dans un widget différent:

Column(
  children: const [
    ImageSection(),
    TitleSection(),
    TextSection(),
    IconSection(),
    HotelSection(),
    ButtonSection(),
    SizedBox(height: 50),
  ],
),

Voilà le code complet de mon widget DemoPage() avec le détail de chaque section:

import 'package:flutter/material.dart';

class DemoPage extends StatelessWidget {
  const DemoPage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Voyage Thailande"),
        actions: [
          IconButton(
            icon: const Icon(
              Icons.bookmark_border,
            ),
            onPressed: () {},
          )
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          children: const [
            ImageSection(),
            TitleSection(),
            TextSection(),
            IconSection(),
            HotelSection(),
            ButtonSection(),
            SizedBox(height: 50),
          ],
        ),
      ),
    );
  }
}

class ImageSection extends StatelessWidget {
  const ImageSection({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Image.network(
        'https://drissas.com/wp-content/uploads/2021/08/photo_thailande.jpeg');
  }
}

class TitleSection extends StatelessWidget {
  const TitleSection({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      child: Row(
        children: [
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const [
              Text(
                'Bienvenue au paradis',
                style: TextStyle(fontSize: 25, fontWeight: FontWeight.w800),
              ),
              Text(
                'Réservez votre séjour en Thailande',
                style: TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class TextSection extends StatelessWidget {
  const TextSection({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.fromLTRB(30, 0, 30, 0),
      child: const Text(
        "La Thaïlande, en forme longue le Royaume de Thaïlande, est un pays d'Asie du Sud-Est dont le territoire couvre 514 000 km2. Avant 1939, il s'appelait le Royaume de Siam. Il est bordé à l'ouest par la Birmanie, au sud par la Malaisie, ",
        softWrap: true,
      ),
    );
  }
}

class IconSection extends StatelessWidget {
  const IconSection({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.fromLTRB(0, 0, 0, 10),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Container(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                Icon(
                  Icons.hotel,
                  color: Theme.of(context).primaryColor,
                ),
                const SizedBox(height: 5),
                Text(
                  'Hotels'.toUpperCase(),
                  style: TextStyle(
                    color: Theme.of(context).primaryColor,
                  ),
                )
              ],
            ),
          ),
          Container(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                Icon(Icons.airplanemode_active,
                    color: Theme.of(context).primaryColor),
                const SizedBox(height: 5),
                Text(
                  'Vols'.toUpperCase(),
                  style: TextStyle(color: Theme.of(context).primaryColor),
                )
              ],
            ),
          ),
          Container(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                Icon(Icons.drive_eta, color: Theme.of(context).primaryColor),
                const SizedBox(height: 5),
                Text(
                  'Voiture'.toUpperCase(),
                  style: TextStyle(color: Theme.of(context).primaryColor),
                )
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class HotelSection extends StatelessWidget {
  const HotelSection({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.fromLTRB(10, 0, 10, 30),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(8.0),
            child: Image.network(
                'https://drissas.com/wp-content/uploads/2021/08/photo_thailande_1.jpeg'),
          ),
          ClipRRect(
            borderRadius: BorderRadius.circular(8.0),
            child: Image.network(
                'https://drissas.com/wp-content/uploads/2021/08/photo_thailande_2.jpeg'),
          ),
        ],
      ),
    );
  }
}

class ButtonSection extends StatelessWidget {
  const ButtonSection({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        textStyle: const TextStyle(fontSize: 20),
        padding: const EdgeInsets.fromLTRB(40, 10, 40, 10),
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.all(Radius.circular(30)),
        ),
      ),
      child: const Text('Voir plus de logements'),
      onPressed: () {},
    );
  }
}

Vous devriez maintenant pouvoir visualiser votre thème couleur sur une page concrète avec différents widgets.

C'est notamment votre appBar qui changera de couleur, avec également le bouton en bas et les icônes de démonstration.

Vous pouvez aussi personnaliser encore plus le thème de votre page, en colorant le texte ou le bord de vos images.

Mais je vous conseille de reste quand même sobre dans la gestion des thèmes couleurs, et de laisser à Flutter le soin de gérer les couleurs du texte par exemple.

De cette manière, le thème clair et foncé sera bien fonctionnel et permettra de lire toutes vos informations peut importe le thème choisi.

Tuto Flutter BLoC Débutant

Voilà donc qui conclut notre tuto flutter bloc débutant sur la création d'une application de gestion de thème couleur.

Si ce n’est pas déjà fait, commencez le cours gratuit Flutter pour recevoir 6 Vidéos de formation et découvrir comment passer vos applications Flutter au niveau supérieur

Rejoignez le cours complet Flutter Académie si vous souhaitez créer des applications ultra-avancées et faire décoller votre entreprise