Article original : How to Build an Always Listening Network Connectivity Checker in Flutter using BLoC
De nombreuses applications mobiles nécessitent une connexion internet stable pour offrir une expérience utilisateur fluide. En tant que développeur Flutter, vous avez besoin d'un moyen robuste pour gérer les changements d'état du réseau, en garantissant que votre application réagisse de manière appropriée, qu'elle soit connectée, déconnectée ou en transition entre deux états.
Cet article vous propose un guide détaillé pour construire un vérificateur de connectivité réseau complet en utilisant une combinaison puissante de packages Flutter modernes et de patrons architecturaux.
Nous exploiterons :
connectivity_plus: Un package pour vérifier la connectivité réseau de base (par exemple, WiFi, données mobiles, Ethernet).internet_connection_checker: Un outil plus fiable qui va au-delà d'une simple vérification réseau en effectuant un ping actif vers une URL connue pour confirmer l'accès réel à Internet.Un appel HTTP direct vers une URL de confiance (comme Google) : En tant que solution de secours, un appel réseau direct peut servir de confirmation finale de la connectivité.
rxdartavecdebounce: Pour éviter des vérifications réseau excessives et rapides, qui peuvent être inefficaces et épuiser la batterie de l'appareil.Injection de Dépendances avec
get_itetinjectable: Pour une base de code propre, modulaire et testable.Gestion d'état avec BLoC et
freezed: Le patron BLoC sépare la logique métier de l'interface utilisateur, etfreezedsimplifie la création d'états et d'événements immuables.Streams : Pour permettre une approche réactive "à l'écoute permanente" des changements de statut réseau.
fluttertoast: Pour fournir un retour utilisateur clair et non intrusif.
Plongeons dans le vif du sujet.
Table des matières :
Étape 1 : Configurer l'Injection de Dépendances avec get_it et injectable
Étape 2 : Implémenter le vérificateur de connectivité réseau
Prérequis
Avant de commencer, assurez-vous d'avoir une compréhension de base de :
Flutter et Dart : Les fondamentaux de la création d'applications avec Flutter.
Programmation Asynchrone : Les concepts tels que
async,awaitetFuture.Patron BLoC : Les principes de base du BLoC (Business Logic Component) pour la gestion d'état.
Génération de code : Comment utiliser des packages comme
build_runnerpour générer du code répétitif (boilerplate).
Étape 1 : Configurer l'Injection de Dépendances avec get_it et injectable
L'Injection de Dépendances (DI) est un patron de conception logicielle qui permet à une classe de recevoir ses dépendances depuis une source externe plutôt que de les créer elle-même. Cela rend votre code plus flexible, réutilisable et plus facile à tester.
Examinons les deux outils que nous utiliserons pour implémenter cela :
get_itest un "localisateur de services" (service locator) qui agit comme un registre central. Vous enregistrez vos services (dépendances) auprès deget_it, et il fournit un moyen de récupérer leur instance unique de n'importe où dans votre application. C'est une alternative simple et efficace aux frameworks de DI plus complexes.injectableest un package de génération de code qui fonctionne avecget_it. En annotant vos classes avec@injectable,@lazySingletonou@module,injectableécrit automatiquement le code répétitif pour enregistrer vos dépendances auprès deget_itpour vous, vous épargnant une configuration manuelle.
Tout d'abord, créez un nouveau projet Flutter comme ceci :
flutter create my_injectable_project
cd my_injectable_project
Ensuite, ajoutez les packages nécessaires à votre fichier pubspec.yaml :
dependencies:
freezed_annotation: ^2.4.1
rxdart: ^0.28.0
get_it: ^7.6.7
injectable: ^2.3.2
internet_connection_checker: ^1.0.0+1
connectivity_plus: ^5.0.2
fluttertoast: ^8.2.4
flutter_bloc: ^8.1.3
http: ^0.13.3
dev_dependencies:
build_runner:
freezed: ^2.4.7
injectable_generator: ^2.4.1
Alors, que se passe-t-il ici ?
freezed_annotation&freezed: Utilisés pour créer des classes de données immuables pour les états et événements BLoC.rxdart: Fournit des opérateurs puissants liés aux flux (streams), incluantdebounceTime, qui est essentiel pour notre vérificateur de connectivité.get_it&injectable: Pour l'injection de dépendances.internet_connection_checker&connectivity_plus: Les packages de base pour vérifier le statut réseau.fluttertoast: Pour afficher des notifications à l'utilisateur.flutter_bloc: Le package BLoC principal.http: Un package pour effectuer des requêtes HTTP, utilisé pour la vérification de l'URL Google.build_runner: L'outil en ligne de commande qui exécute les générateurs de code.injectable_generator: Le générateur qui fonctionne avecinjectable.
Il est maintenant temps de créer le fichier de configuration de l'injection. Créez un fichier, par exemple lib/core/dependency_injection/injection.dart, pour configurer get_it et injectable.
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'package:my_injectable_project/core/dependency_injection/injection.config.dart';
// The global instance of GetIt
final GetIt getIt = GetIt.instance;
// The annotation @injectableInit tells injectable to generate the init method
@injectableInit
void configureDependencies(String env) => getIt.init(environment: env);
final GetIt getIt = GetIt.instance;: Nous créons une instance statique deGetItaccessible globalement.@injectableInit: Cette annotation signale àinjectable_generatorque c'est dans ce fichier qu'il doit générer le code d'enregistrement des dépendances.void configureDependencies(String env) => getIt.init(environment: env);: Cette fonction initialiseget_itet nous permet de le configurer pour différents environnements (ex: 'dev', 'prod').
Enfin, nous devons créer un module pour les dépendances. Créez un fichier de module, par exemple lib/core/dependency_injection/register_module.dart, pour enregistrer les classes tierces qui n'appartiennent pas à la structure de votre propre projet.
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
import 'package:my_injectable_project/core/network_info/network_info.dart';
import 'package:my_injectable_project/features/network_info/bloc/network_info_bloc.dart';
// The @module annotation marks this class as a module for injectable
@module
abstract class RegisterModule {
// A @lazySingleton means the instance will be created only when it's first requested
@lazySingleton
Connectivity get connectivity => Connectivity();
@lazySingleton
InternetConnectionChecker get internetConnectionChecker => InternetConnectionChecker();
@lazySingleton
NetworkInfoImpl get networkInfo => NetworkInfoImpl(
connectivity: connectivity,
internetConnectionChecker: internetConnectionChecker,
);
@lazySingleton
NetworkInfoBloc get networkInfoBloc => NetworkInfoBloc(
networkInfo: getIt<NetworkInfo>(),
connectivity: getIt<Connectivity>(),
);
}
@module: Une annotation spéciale qui marque une classe comme module pourinjectable. Les modules sont utiles pour enregistrer des classes tierces ou créer des instances de classes nécessitant une configuration complexe.@lazySingleton: Cette annotation indique àinjectablede créer une instance unique de la classe et de la réutiliser à chaque fois qu'elle est demandée. La partie "lazy" signifie que l'instance n'est créée que lorsqu'elle est nécessaire pour la première fois.
Étape 2 : Implémenter le vérificateur de connectivité réseau
Interface et Implémentation
C'est une bonne pratique de programmer par rapport à une interface plutôt qu'à une implémentation concrète. Cela permet de remplacer facilement les implémentations et simplifie les tests. Ci-dessous, lib/core/network_info/network_info.dart est la classe abstraite tandis que lib/core/network_info/network_info_impl.dart est l'implémentation. C'est ici que réside la fonctionnalité du flux, que le bloc utilise.
lib/core/network_info/network_info.dart :
// The abstract class defines the contract for our network checker
abstract class NetworkInfo {
Future<bool> get isConnected;
}
lib/core/network_info/network_info_impl.dart :
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:injectable/injectable.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
import 'package:my_injectable_project/core/network_info/network_info.dart';
// @LazySingleton(as: NetworkInfo) tells injectable to register this class
// as a lazy singleton, and to provide it when a NetworkInfo is requested.
@LazySingleton(as: NetworkInfo)
class NetworkInfoImpl implements NetworkInfo {
final Connectivity connectivity;
final InternetConnectionChecker internetConnectionChecker;
const NetworkInfoImpl({
required this.connectivity,
required this.internetConnectionChecker,
});
@override
Future<bool> get isConnected async {
try {
bool isDeviceConnected = false;
// First, check the connectivity type (WiFi, mobile, etc.)
final connectivityResult = await connectivity.checkConnectivity();
debugPrint('Connectivity Result: $connectivityResult');
if (connectivityResult != ConnectivityResult.none) {
// If there's a network type, verify actual internet access
isDeviceConnected = await internetConnectionChecker.hasConnection ||
await hasInternetConnection();
}
debugPrint('Device Connected: $isDeviceConnected');
return isDeviceConnected;
} catch (e) {
debugPrint('Error checking network connection: $e');
return false;
}
}
// A redundant but useful check with a direct HTTP call
Future<bool> hasInternetConnection() async {
try {
final response = await http.get(Uri.parse('https://www.google.com')).timeout(
const Duration(seconds: 5),
);
if (response.statusCode == 200) {
return true;
}
} catch (e) {
debugPrint('Error checking internet connection: $e');
}
return false;
}
}
@LazySingleton(as: NetworkInfo): C'est l'annotation clé. Elle enregistreNetworkInfoImplcomme un singleton qui implémente l'interfaceNetworkInfo. LorsquegetIt<NetworkInfo>()est appelé, une instance deNetworkInfoImplsera fournie.connectivity.checkConnectivity(): Fournit une vérification rapide du type de connexion de l'appareil.internetConnectionChecker.hasConnection: Ce package est plus fiable que la simple vérification du type de réseau, car un appareil peut être "connecté" à un réseau WiFi sans avoir d'accès Internet.internet_connection_checkereffectue un ping actif sur une série d'adresses pour vérifier.hasInternetConnection(): Une fonction de secours qui effectue une requête HTTP directe vers une URL fiable comme Google. Cela fournit une couche de vérification supplémentaire.
Étape 3 : Créer le BLoC pour la connectivité réseau
Le BLoC gère la logique métier de la vérification du statut réseau et émet les changements d'état appropriés vers l'interface utilisateur.
lib/features/network_info/bloc/network_info_bloc.dart :
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:my_injectable_project/core/network_info/network_info.dart';
import 'package:rxdart/rxdart.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'network_info_bloc.freezed.dart';
part 'network_info_event.dart';
part 'network_info_state.dart';
// @injectable marks this class to be registered by injectable
@injectable
class NetworkInfoBloc extends Bloc<NetworkInfoEvent, NetworkInfoState> {
final NetworkInfo networkInfo;
final Connectivity connectivity;
late StreamSubscription<List<ConnectivityResult>> connectivitySubscription;
NetworkInfoBloc({
required this.networkInfo,
required this.connectivity,
}) : super(NetworkInfoState.initial()) {
// Custom event transformer for debouncing
EventTransformer<T> debounce<T>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
// The 'on' method maps events to states
on<CheckNetwork>(
_onCheckNetwork,
// Apply the debounce transformer to limit the rate of function calls
transformer: debounce(
const Duration(seconds: 1),
),
);
// Listen to changes from the connectivity_plus package
connectivitySubscription = connectivity.onConnectivityChanged.listen((connectivityResult) async {
await Future.delayed(const Duration(seconds: 1)); // Small delay to avoid race conditions
debugPrint('Connectivity Result after delay: $connectivityResult');
add(const CheckNetwork());
});
}
// The event handler for CheckNetwork
Future<void> _onCheckNetwork(
CheckNetwork event,
Emitter<NetworkInfoState> emit,
) async {
final isConnected = await networkInfo.isConnected;
// Only emit a new state if the network status has actually changed
if (state.networkStatus != isConnected) {
emit(state.copyWith(networkStatus: isConnected));
}
debugPrint(
'Network Status ==> ${isConnected ? "Data connection is available." : "You are disconnected from the internet."}');
}
@override
Future<void> close() {
// It's crucial to cancel the stream subscription to prevent memory leaks
connectivitySubscription.cancel();
return super.close();
}
}
EventTransformer<T> debounce<T>(Duration duration): Il s'agit d'un transformateur personnalisé. Il utilise l'opérateurdebounceTimederxdartpour attendre une durée d'inactivité spécifiée avant de permettre le traitement de l'événement. C'est parfait pour éviter une cascade de vérifications réseau.connectivity.onConnectivityChanged.listen(...): Cela crée un abonnement à un flux deConnectivityResult. Chaque fois que le statut de connectivité de l'appareil change (par exemple, passage du WiFi aux données mobiles), ce flux émet une nouvelle valeur, qui à son tour déclenche notre événementCheckNetwork._onCheckNetwork(...): Cette fonction est le cœur de la logique du BLoC. Elle appellenetworkInfo.isConnectedpour obtenir le statut actuel et émet ensuite un nouvel état si le statut a changé.close(): Surcharger cette méthode est vital pour une gestion appropriée des ressources. C'est ici que nous nettoyons notreStreamSubscriptionpour éviter les fuites de mémoire.
Événements et États
freezed est un outil de génération de code qui facilite la création de classes de données immuables, essentielles pour le patron BLoC.
lib/features/network_info/bloc/network_info_event.dart :
part of 'network_info_bloc.dart';
@freezed
class NetworkInfoEvent with _$NetworkInfoEvent {
const factory NetworkInfoEvent.checkNetwork() = CheckNetwork;
}
@freezed: Cette annotation déclenchefreezedpour générer le code répétitif de cette classe.const factory NetworkInfoEvent.checkNetwork() = CheckNetwork;: Définit un événement unique pour notre BLoC, qui estCheckNetwork.
lib/features/network_info/bloc/network_info_state.dart :
part of 'network_info_bloc.dart';
@freezed
class NetworkInfoState with _$NetworkInfoState {
const factory NetworkInfoState({required bool networkStatus}) = _NetworkInfoState;
factory NetworkInfoState.initial() => const NetworkInfoState(
networkStatus: true,
);
}
const factory NetworkInfoState(...): Définit notre état, qui contient simplement un booléennetworkStatus.factory NetworkInfoState.initial(): Une usine (factory) utilitaire pour créer l'état initial du BLoC.
Exécuter le générateur de code
Pour générer les fichiers *.freezed.dart et *.g.dart, exécutez la commande suivante dans votre terminal :
flutter pub run build_runner build --delete-conflicting-outputs
Cette commande surveillera votre projet pour détecter les changements et régénérera automatiquement les fichiers nécessaires.
Étape 4 : Intégrer le BLoC à l'interface utilisateur (UI)
Enfin, nous allons connecter notre BLoC à l'UI Flutter pour réagir aux changements d'état.
Dans votre widget principal, par exemple main.dart, vous pouvez accéder au BLoC via getIt.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:my_injectable_project/core/dependency_injection/injection.dart';
import 'package:my_injectable_project/features/network_info/bloc/network_info_bloc.dart';
void main() {
// Initialize dependency injection
configureDependencies('dev');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Provide the BLoC to the widget tree
return BlocProvider(
create: (context) => getIt<NetworkInfoBloc>(),
child: MaterialApp(
title: 'Network Checker Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const NetworkCheckerPage(),
),
);
}
}
class NetworkCheckerPage extends StatefulWidget {
const NetworkCheckerPage({super.key});
@override
State<NetworkCheckerPage> createState() => _NetworkCheckerPageState();
}
class _NetworkCheckerPageState extends State<NetworkCheckerPage> {
final NetworkInfoBloc networkInfoBloc = getIt<NetworkInfoBloc>();
@override
void initState() {
super.initState();
// Listen to the BLoC's state stream
networkInfoBloc.stream.listen((state) {
if (state.networkStatus) {
toastInfo(
msg: "Data connection is available.",
status: Status.success,
);
} else {
toastInfo(
msg: "You are disconnected from the internet.",
status: Status.error,
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Network Connectivity'),
),
body: Center(
child: BlocBuilder<NetworkInfoBloc, NetworkInfoState>(
builder: (context, state) {
return Text(
state.networkStatus
? 'Connected to the Internet!'
: 'Disconnected from the Internet.',
style: TextStyle(
fontSize: 20,
color: state.networkStatus ? Colors.green : Colors.red,
),
);
},
),
),
);
}
}
BlocProvider: Ce widget fournit l'instance deNetworkInfoBlocà l'arborescence des widgets, le rendant accessible à n'importe quel widget enfant viaBlocProvider.of<NetworkInfoBloc>(context).networkInfoBloc.stream.listen(...): DansinitState, nous nous abonnons au flux d'état du BLoC. Chaque fois qu'un nouvel état est émis (ce qui arrive quand le statut réseau change), notre écouteur est déclenché, et nous pouvons afficher une notification toast.BlocBuilder: Ce widget est utilisé pour reconstruire l'UI en réponse aux changements d'état. Il écoute les nouveaux états duNetworkInfoBlocet reconstruit sa fonctionbuilder, mettant à jour le widgetTextpour refléter le statut réseau actuel.
Étape 5 : Afficher des notifications Toast
Le package fluttertoast fournit un moyen simple et indépendant de la plateforme pour afficher des messages non intrusifs.
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
enum Status { success, error }
void toastInfo({
required String msg,
required Status status,
}) {
Fluttertoast.showToast(
msg: msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: status == Status.success ? Colors.green : Colors.red,
textColor: Colors.white,
fontSize: 16.0,
);
}
Cette fonction utilitaire simplifie le processus d'affichage des toasts en vous permettant de spécifier un message et un statut, lequel détermine la couleur d'arrière-plan.
Conclusion
En combinant la puissance du patron BLoC, de l'injection de dépendances avec get_it et injectable, et des bibliothèques de vérification réseau robustes, vous pouvez construire un vérificateur de connectivité réseau hautement fiable et maintenable dans votre application Flutter.
Cette architecture garantit que votre application est réactive aux changements de réseau et offre une séparation claire des préoccupations, rendant votre base de code évolutive et facile à tester.
Références
Voici quelques références qui soutiennent les concepts et packages utilisés dans cet article :
Fondamentaux Flutter et Dart :
Documentation officielle Flutter : Fournit des guides complets sur le développement Flutter, incluant les widgets, la gestion d'état et la programmation asynchrone.
Documentation officielle Dart : Détaille les fonctionnalités du langage Dart, incluant la programmation asynchrone avec
FutureetStream.
Connectivité et Vérification Réseau :
Package
connectivity_plussur Pub.dev : Documentation officielle du pluginconnectivity_plus, expliquant son utilisation pour vérifier les types de connectivité réseau.Package
internet_connection_checkersur Flutter Gems : Détails sur le packageinternet_connection_checker, qui vérifie l'accès réel à Internet en effectuant un ping sur des serveurs externes.Package
httpsur Pub.dev : La documentation officielle pour effectuer des requêtes HTTP en Dart et Flutter.
Injection de Dépendances :
Package
get_itsur Pub.dev : La documentation officielle deget_it, un localisateur de services simple pour Dart et Flutter.Package
injectablesur Pub.dev : La documentation officielle d'injectable, un générateur de code pourget_itqui simplifie l'enregistrement des dépendances.Gestion d'état (BLoC) : Package
flutter_blocsur Pub.dev – la documentation officielle du packageflutter_bloc, fournissant des widgets et utilitaires pour implémenter le patron BLoC.
Immuabilité et Génération de Code :
Package
freezedsur Pub.dev : La documentation officielle defreezed, un puissant générateur de code pour créer des classes de données immuables.Package
build_runnersur Pub.dev : L'outil utilisé pour exécuter les générateurs de code commeinjectable_generatoretfreezed.
Programmation Réactive (RxDart) et Streams :
Package
rxdartsur Pub.dev : Documentation officielle de RxDart, qui étend l'API Stream de Dart avec des opérateurs puissants commedebounceTime."Classe Stream - bibliothèque dart:async" sur Flutter API Docs : La documentation officielle de Dart pour la classe
Stream.
Retour Utilisateur :
- Package
fluttertoastsur Pub.dev : Documentation officielle du packagefluttertoastpour afficher des messages toast.