Utiliser Hive et Flutter pour le stockage local
📰

Utiliser Hive et Flutter pour le stockage local

Tags
Flutter
Dart
Tutoriel
Base de données
Published
Publié le ‘Feb 17, 2022
Updated At
Mar 17, 2022 09:57 PM
 
Logo officiel de Hive
Logo officiel de Hive
 

Qu’est ce que Hive ?

Hive est un système de gestion de base de données NoSQL écrit en Dart. Il a la faculté d’être très rapide, surtout en lecture. Il fonctionne sur un principe de clé-valeur et peut être un excellent moyen de conserver de façon locale des informations sur le système de l’utilisateur.
💡
Le NoSQL est un type de base de données qui s’oppose au SQL en ce sens qu’on n’a pas besoin d’y définir de façon figée le schéma des données qui y seront stockées. Ce qui donne une certaine flexibilité au stockage, permettant ainsi de pouvoir gérer des données dont la nature et les constituants ne sont pas fixes.
Un autre point fort de Hive réside dans le fait que vous pouvez littéralement grâce à lui stocker et récupérer des objets sans asynchronisme, donc sans perte de temps. À cela, s’ajoute le fait que Hive dispose d’un générateur de code. Ce qui vous permet de vous concentrer uniquement sur la structure des objets que vous voulez stocker et sur les opérations qui leur sont appliquées tout en limitant le nombre d’erreurs pouvant se produire en cours d’exécution.
Il fonctionne également très bien sur toutes les plateformes supportées par Flutter.

Ce que nous allons faire 🚩

Nous allons mettre en place de quoi gérer un utilisateur et ses identifiants. Il y aura donc une page d’accueil et une page d’information qui va afficher les informations de l’utilisateur. On va donc récupérer les informations de l’utilisateur dans un premier temps, les afficher et ensuite les supprimer dans la page d’information.
Nous allons partir de ce dossier de base pour la suite du projet:
 
Le code source final est disponible juste ici également:
 
Le dossier debut contient les pages ci-dessous:
home_page.dart
home_page.dart
infos_page.dart
infos_page.dart

Le nécessaire pour utiliser Hive

Sans plus tarder, nous verront comment utiliser Hive pour faire des opérations de base: créer, lire, modifier, et supprimer (ce qu’on appelle le “CRUD”).
 

Préparation de l’environnement

Dans votre terminal / invite de commandes, entrez les commandes suivantes pour installer les dépendances dont nous aurons besoin pour utiliser Hive:
flutter pub add hive
flutter pub add hive_flutter
flutter pub add --dev build_runner
flutter pub add --dev hive_generator
 
Vous devriez obtenir ceci dans le fichier pubspec.yaml:
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  hive: ^2.0.5
  hive_flutter: ^1.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^1.0.0
  build_runner: ^2.1.7
  hive_generator: ^1.1.2
 

Mise en place

On va l’appeler AppUser. Et elle devrait contenir le code ci-dessous si nous prenons en compte que nous stockons le nom d’utilisateur et le mot de passe de ce dernier. Nous allons créer un dossier models à l’intérieur de lib et y mettre le fichier app_user.dart.
class AppUser {
  String username;
  String password;

  AppUser({
    required this.username,
    required this.password,
  });
}
app_user.dart
Cette classe va nous servir de base. Les objets de cette classe seront stockés du côté de l’utilisateur. Et comment cela va se faire ?
Nous allons rajouter un décorateur spécial, HiveType, qui vient du package Hive directement. Il va servir à Hive pour pouvoir nous générer le code nécessaire pour pouvoir effectuer des opérations CRUD avec nos objets sur le périphérique du client. Nous allons également définir notre classe AppUser comme une héritière de la classe HiveObject.
/// On importe le package Hive
import 'package:hive/hive.dart';

/// On rajoute le décorateur HiveType auquel
/// on va donner un id qui sera dans notre cas 0.
/// Pour vos prochaines classes, il suffira de mettre 
/// une autre valeur différente de 0, chaque classe
/// étant associée à un id entier unique
@HiveType(typeId: 0)
/// Et pour finir nous avons définit AppUser comme une classe
/// héritière de HiveObject
class AppUser extends HiveObject {
  String username;
  String password;

  AppUser({
    required this.username,
    required this.password,
  });
}
La prochaine étape sera de rajouter des décorateurs HiveField aux différents attributs de la classe AppUser. Cela servira à Hive pour conserver les différents champs de nos objets et les retrouver depuis le stockage:
import 'package:hive/hive.dart';

@HiveType(typeId: 0)
class AppUser extends HiveObject {
	/// Encore une fois nous allons assigner des identifiants uniques
	/// entiers à nos attributs
	@HiveField(0)
  String username;
	
	@HiveField(1)
  String password;

  AppUser({
    required this.username,
    required this.password,
  });
}
Voilà ! Maintenant nous allons auto-générer le code qui va transformer notre classe AppUser de sorte à pouvoir permettre à tous les objets de cette classe de facilement être stockés et lus par Hive.
Cela va se faire en deux étapes:
  • On va d’abord rajouter une ligne après avoir importé Hive:
import 'package:hive/hive.dart';

/// Cette ligne-ci.
/// Il est important qu'elle soit formatée comme suit:
/// nom_du_fichier.g.dart
part 'app_user.g.dart';

@HiveType(typeId: 0)
class AppUser extends HiveObject {
  @HiveField(0)
  String username;
  @HiveField(1)
  String password;

  AppUser({
    required this.username,
    required this.password,
  });
}
  • Et ensuite, revenir dans notre invite de commandes et entrer la commande suivante:
flutter pub run build_runner build
💡
Si en exécutant cette commande vous rencontrez des erreurs, veuillez vérifier le nom de votre fichier et / ou essayez cette commande
 flutter pub run build_runner build --delete-conflicting-outputs
Vous devriez à présent remarquer qu’un nouveau fichier est apparu dans votre dossier models du nom de app_user.g.dart avec un contenu qui doit ressembler à ça:
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'app_user.dart';

// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************

class AppUserAdapter extends TypeAdapter<AppUser> {
  @override
  final int typeId = 0;

  @override
  AppUser read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return AppUser(
      username: fields[0] as String,
      password: fields[1] as String,
    );
  }

  @override
  void write(BinaryWriter writer, AppUser obj) {
    writer
      ..writeByte(2)
      ..writeByte(0)
      ..write(obj.username)
      ..writeByte(1)
      ..write(obj.password);
  }

  @override
  int get hashCode => typeId.hashCode;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is AppUserAdapter &&
          runtimeType == other.runtimeType &&
          typeId == other.typeId;
}
Ne vous en faîtes pas 😅, n’essayez pas de vouloir comprendre le contenu tout de suite. Sachez surtout qu’il va beaucoup nous aider ce fichier.
 

Configurer notre application pour Hive et AppUser

Nous avons déjà le nécessaire pour utiliser Hive. Il nous reste maintenant à initialiser et enregistrer AppUser dans Hive. Pour cela, importons d'abord hive_flutter et notre fichier app_user.dart dans le fichier main.dart.
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hive_tutoriel/models/app_user.dart';
Puis modifier notre main.dart:
/// On rend la fonction main asynchrone
void main() async {
	/// On initialise Hive pour notre application
  await Hive.initFlutter();
	
	/// On enregistre notre AppUser grâce à son adaptateur
	/// qui s'est autogénéré dans le fichier app_user.g.dart
  Hive.registerAdapter(AppUserAdapter());
	
	/// Ensuite on va ouvrir la "boîte" contenant les objets AppUser
	/// et donner un nom à cette boîte: "user" dans notre cas.
	/// Ce nom est arbitraire et vous pouvez mettre autre chose
  await Hive.openBox<AppUser>("user");

  runApp(const App());
}
 

Un petit focus sur Hive

Dans les commentaires du code précédent j’ai parlé de “boîtes” dans lesquelles Hive range les objets de la même classe. En effet, Hive range les objets de la même classe dans des boxes (pluriel de box), chaque box correspondant à une classe précise. Pour pouvoir donc accéder à un objet précis il faudra donc ouvrir au préalable la “boîte” contenant les objets de la classe désirés et ensuite trouver et récupérer l’objet que nous désirons. Notez aussi que chaque boîte ouverte est identifiable grâce à un nom unique que nous définissons nous-mêmes comme nous l’avons fait à cette ligne où nous avons donné le nom “user” à la boîte qui va accueillir les objets de la classe AppUser;
  await Hive.openBox<AppUser>("user");
💡
N’oubliez pas de toujours utiliser la méthode openBox au moins une fois, de sorte à la garder ouvert, avant de vouloir avoir accès à une box. Je recommande de le faire dans la fonction main de main.dart comme dans notre exemple ci-dessus !

Réaliser un CRUD avec Hive 😎

La partie la plus importante pour vous mes chers lecteurs.
Commençons déjà par mettre en place de quoi récupérer les informations de la page d’accueil et former une classe AppUser avec. Nous allons donc ouvrir le fichier home_page.dart du dossier pages qui doit ressembler à ça.
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  /// Une clé qui va nous permettre de gérer notre
  /// formulaire
  final _formKey = GlobalKey<FormState>();

  /// Les controllers qui vont nous servir à recueillir
  /// les entrées de l'utilisateur
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Connexion"),
      ),
      body: Form(
        key: _formKey,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 50),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                controller: _usernameController,
                decoration: const InputDecoration(
                  hintText: "Nom d'utilisateur",
                ),
                // Pour éviter que les champs soient vides
                validator: (value) {
                  if (value!.isEmpty) return "Ce champ ne peut être vide";
                  return null;
                },
              ),
              const Gap(10),
              TextFormField(
                controller: _passwordController,
                decoration: const InputDecoration(
                  hintText: "Mot de passe",
                ),
                obscureText: true,
                validator: (value) {
                  if (value!.isEmpty) return "Ce champ ne peut être vide";
                  return null;
                },
              ),
              const Gap(20),
              ElevatedButton(
                style: ElevatedButton.styleFrom(
                    fixedSize:
                        Size.fromWidth(MediaQuery.of(context).size.width)),
                onPressed: () {},
                child: const Text(
                  "Enregister",
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
Nous allons nous concentrer sur la fonction onPressed du widget ElevatedButton, rajouter la validation des différents champs du formulaire et créer un nouvel objet à partir de la classe AppUser. Cet objet sera composé par l’username et le mot de passe récupérés respectivement par _usernameController et _passwordController.
() {
	if (_formKey.currentState!.validate()) {
		AppUser user = AppUser(
      username: _usernameController.text,
      password: _passwordController.text,
    );
	}
}
Maintenant que nous avons notre classe et que notre boîte a déjà été ouverte dans la fonction main, nous allons la récupérer. C’est assez simple...
/// On rend notre fonction main asynchrone en vue d'un accès à Hive
() async {
	if (_formKey.currentState!.validate()) {
		AppUser user = AppUser(
      username: _usernameController.text,
      password: _passwordController.text,
    );
		/// Nous avons enfin notre très chèr box de type AppUser
		Box<AppUser> box = Hive.box('user');
	}
}
 

Ecrire

Pour stocker un objet dans une box vous avez deux choix:
  • Le premier c’est juste de l’ajouter dans la boîte:
await box.add(user);
  • Le second consiste à l’ajouter dans la boîte en précisant un index qui est un entier ou une clé précise qui peut être de type variable:
/// Quand on précise l'index
await box.putAt(0, user);
/// Quand on veut préciser une clé
await box.put('key', user);
Vous pouvez aussi vérifier si la boîte contient déjà des éléments ou pas.
if (box.isEmpty) {
	/// si la boîte est vide
}

if (box.isNotEmpty) {
	/// si la boîte n'est pas vide
}
Pour les buts de notre objectif nous allons rajouter le code suivant à notre projet:
if (_formKey.currentState!.validate()) {
      AppUser user = AppUser(
        username: _usernameController.text,
        password: _passwordController.text,
      );

      Box<AppUser> box = Hive.box('user');

			/// Une simple condition pour agir suivant si la box est vide
			/// et agir suivant les cas
      if (box.isNotEmpty) {
				/// Pour mettre notre user à la position 0 dans la box des AppUser
        await box.putAt(0, user);
      } else {
				// Juste pour ajouter notre user si la box est vide
        await box.add(user);
      }

			/// Pour ouvrir la page d'information après avoir
			/// sauvegardé notre user
			Navigator.of(context).push(
	      MaterialPageRoute(
	        builder: (_) => const InfosPage(),
	      ),
	    );
}
💡
À ce niveau vous pouvez rajouter de quoi gérer l’interface et mettre peut-être un CircularProgressIndicator pour plus de réalisme. Chose qui sera présente dans la version finale du projet.
 

Lire

La lecture des informations gérées par Hive est assez simple.
/// On récupère la boîte qui nous intéresse
Box<AppUser> box = Hive.box('user');

// Pour récupérer user à la position 0
AppUser? user = box.getAt(0);
// Pour récupérer user grâce à une clé
AppUser? user = box.get('key');
Nous allons l’intégrer de façon simple notre page d’information.
class InfosPage extends StatefulWidget {
  const InfosPage({
    Key? key,
  }) : super(key: key);

  @override
  State<InfosPage> createState() => _InfosPageState();
}

class _InfosPageState extends State<InfosPage> {
	/// La variable qui va nous servir à récupérer notre user
	AppUser? user;
	
	/// On ouvre la box des AppUser
  Box<AppUser> box = Hive.box('user');
	
	/// On rajoute la méthode initState()
	/// qui est une méthode propre aux widgets et
  /// qui se lance lors de la construction de la page
	@override
  void initState() {
    super.initState();
		/// Nous nous servons d'un try-catch 
		/// pour éviter les erreurs de lecture au cas où notre
		/// box n'aurait rien à la position 0.
		/// Notre variable user restera null si elle n'a pas pu récupérer
		/// d'objet AppUser dans la box.
    try {
      user = box.getAt(0);
    } catch (e) {

    }
  }

	@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Mes infos"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
					/// Pour dire de ne rien nous afficher si user est null
          children: user == null
             ? []
            : <Widget>[
						/// Là nous affichons nos résultats
						Text(
              "Username: ${user!.username.toString()}",
            ),
	          Text(
              "Password: ${user!.password.toString()}",
            ),
						Gap(10),
            ElevatedButton(
              style: ElevatedButton.styleFrom(
                fixedSize: Size.fromWidth(MediaQuery.of(context).size.width),
              ),
              onPressed: () {},
              child: const Text(
                "Supprimer",
              ),
            )
          ],
        ),
      ),
    );
  }
}
💡
Hive vous permet de lire directement depuis le stockage sans avoir besoin d’un await 🎉. Ce qui ne ralentit pas votre application et vous permet d’aller beaucoup plus vite sans vous souciez du temps de lecture.

Supprimer

La suppression est aussi relativement simple
/// Pour supprimer à un index précis
await box.deleteAt(0);
/// Pour supprimer grâce à une clé
await box.delete('key');
/// Pour supprimer complètement la box du stockage de l'appareil
await box.deleteFromDisk();
On pourrait donc s’en servir simplement et la rajouter à la méthode onPressed de notre ElevatedButton dans la base d’infos qui sert à supprimer les données.
() async {
	await box.deleteAt(0);
}
💡
A ce niveau vous pouvez rajouter de quoi gérer l’interface et mettre peut être un CircularProgressIndicator pour plus de réalisme. Chose qui sera présente dans la version finale du projet.
 

Mettre à jour

Hive ne dispose pas de façons de mettre à jour un objet stocké localement. La seule solution demeure de le supprimer et de le remplacer ou juste de l’écraser avec un autre objet.
💡
Il existe un autre outil de base de données NoSQL, écrit entièrement en Dart, développé par l’équipe de Hive dénommé Isar qui est plus adapté pour les requêtes complexes et plus spécifiques sur le stockage local. Je le recommande pour des projets où l’on manipule beaucoup de données qui ayant des relations entre elles.
 

Démo

Le code source finale est disponible juste ici également:
 
notion image
 

Pour aller plus loin 🏄

  • Si vous définissez une classe qui se retrouve comme type de l’attribut d’une autre classe, vous aurez besoin de rendre cette première classe “stockable” par Hive.
  • Penser à une bonne architecture de projet tout en utilisant Hive est important. Je vous propose ce bout de code que j’ai écrit pour pouvoir interagir avec Hive:

Merci de la lecture ✨

Si tu as apprécié tu peux m’offrir un café juste ici 💙, réagir et commenter en bas si tu as des apports et/ou des corrections. C’est toujours un plaisir et cela m’encourage à écrire plus souvent .
Si tu as apprécié tu peux m’offrir un café juste ici 💙, réagir et commenter en bas si tu as des apports et/ou des corrections. C’est toujours un plaisir et cela m’encourage à écrire plus souvent .

notion image