Many mobile applications require a stable internet connection to provide a smooth user experience. And as a Flutter developer, you need a robust way to handle network state changes, ensuring that your app responds gracefully whether it’s connected, disconnected, or transitioning between states.
This article will give you a detailed walkthrough of building a comprehensive network connectivity checker using a powerful combination of modern Flutter packages and architectural patterns.
We will leverage:
connectivity_plus
: A package to check for basic network connectivity (for example, WiFi, mobile data, Ethernet).internet_connection_checker
: A more reliable tool that goes beyond a simple network check by actively pinging a known URL to confirm actual internet access.A direct HTTP call to a trusted URL (like Google): As a failsafe, a direct network call can serve as a final confirmation of connectivity.
rxdart
withdebounce
: To prevent excessive and rapid network checks, which can be inefficient and drain the device’s battery.Dependency Injection with
get_it
andinjectable
: For a clean, modular, and testable codebase.State Management with BLoC and
freezed
: The BLoC pattern separates business logic from the UI, andfreezed
simplifies the creation of immutable states and events.Streams: To enable a reactive, “always-listening” approach to network status changes.
fluttertoast
: To provide clear, non-intrusive user feedback.
Let’s dive in.
Table of Contents:
Prerequisites
Before you begin, make sure you have a basic understanding of:
Flutter and Dart: The fundamentals of building apps with Flutter.
Asynchronous Programming: Concepts like
async
,await
, andFuture
.BLoC pattern: The core principles of BLoC (Business Logic Component) for state management.
Code generation: How to use packages like
build_runner
to generate boilerplate code.
Step 1: Set Up Dependency Injection with get_it
and injectable
Dependency Injection (DI) is a software design pattern that allows a class to receive its dependencies from an external source rather than creating them itself. This makes your code more flexible, reusable, and easier to test.
Let’s look at the two tools we’ll use to implement this:
get_it
is a “service locator” that acts as a central registry. You register your services (dependencies) withget_it
, and it provides a way to retrieve their single instance from anywhere in your app. It’s a simple and effective alternative to more complex DI frameworks.injectable
is a code-generation package that works withget_it
. By annotating your classes with@injectable
,@lazySingleton
, or@module
,injectable
automatically writes the boilerplate code to register your dependencies withget_it
for you, saving you from manual setup.
First, create a new Flutter project like this:
flutter create my_injectable_project
cd my_injectable_project
Next, add the necessary packages to your pubspec.yaml
file:
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
So what’s going on here?
freezed_annotation
&freezed
: Used for creating immutable data classes for BLoC states and events.rxdart
: Provides powerful stream-related operators, includingdebounceTime
, which is essential for our connectivity checker.get_it
&injectable
: For dependency injection.internet_connection_checker
&connectivity_plus
: The core packages for checking network status.fluttertoast
: To display user notifications.flutter_bloc
: The main BLoC package.http
: A package to make HTTP requests, used for the Google URL check.build_runner
: The command-line tool that runs the code generators.injectable_generator
: The generator that works withinjectable
.
Now it’s time to create the injection configuration file. Go ahead and create a file, for example, lib/core/dependency_injection/injection.dart
, to set up get_it
and 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;
: We create a static instance ofGetIt
that can be accessed globally.@injectableInit
: This annotation signals toinjectable_generator
that this is the file where it should generate the dependency registration code.void configureDependencies(String env) => getIt.init(environment: env);
: This function initializesget_it
and allows us to configure it for different environments (e.g., ‘dev’, ‘prod’).
Lastly, we need to create a module for dependencies. Create a module file, for example, lib/core/dependency_injection/register_module.dart
, to register third-party classes that don’t belong to your own project structure.
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
: A special annotation that marks a class as a module forinjectable
. Modules are useful for registering third-party classes or creating instances of classes that require complex setup.@lazySingleton
: This annotation tellsinjectable
to create a single instance of the class and reuse it every time it is requested. The “lazy” part means the instance is not created until it’s needed for the first time.
Step 2: Implement the Network Connectivity Checker
Interface and Implementation
It’s good practice to program against an interface rather than a concrete implementation. This allows for easy swapping of implementations and makes testing simpler. Below, lib/core/network_info/network_info.dart
is the abstract class while lib/core/network_info/network_info_impl.dart
is the implementation. This is where the functionality of the flow lies, which the bloc uses.
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)
: This is the key annotation. It registersNetworkInfoImpl
as a singleton that implements theNetworkInfo
interface. WhengetIt<NetworkInfo>()
is called, an instance ofNetworkInfoImpl
will be provided.connectivity.checkConnectivity()
: Provides a quick check of the device’s connection type.internetConnectionChecker.hasConnection
: This package is more reliable than just checking the network type, as a device can be “connected” to a WiFi network without having internet access.internet_connection_checker
actively pings a series of addresses to verify.hasInternetConnection()
: A fallback function that makes a direct HTTP request to a reliable URL like Google. This provides an extra layer of verification.
Step 3: Create the BLoC for Network Connectivity
The BLoC handles the business logic of checking the network status and emitting the appropriate state changes to the UI.
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)
: This is a custom transformer. It usesrxdart
‘sdebounceTime
operator to wait for a specified duration of inactivity before allowing the event to be processed. This is perfect for preventing a cascade of network checks.connectivity.onConnectivityChanged.listen(...)
: This creates a subscription to a stream ofConnectivityResult
. Whenever the device’s connectivity status changes (for example, switches from WiFi to mobile data), this stream emits a new value, which in turn triggers ourCheckNetwork
event._onCheckNetwork(...)
: This function is the heart of the BLoC’s logic. It callsnetworkInfo.isConnected
to get the current status and then emits a new state if the status has changed.close()
: Overriding this method is vital for proper resource management. It’s where we clean up ourStreamSubscription
to avoid memory leaks.
Events and States
freezed
is a code-generation tool that makes it easy to create immutable data classes, which are essential for the BLoC pattern.
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
: This annotation triggersfreezed
to generate the boilerplate code for this class.const factory NetworkInfoEvent.checkNetwork() = CheckNetwork;
: This defines a single event for our BLoC, which isCheckNetwork
.
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(...)
: This defines our state, which simply holds anetworkStatus
boolean.factory NetworkInfoState.initial()
: A helper factory to create the initial state of the BLoC.
Run the Code Generator
To generate the *.freezed.dart
and *.g.dart
files, run the following command in your terminal:
flutter pub run build_runner build --delete-conflicting-outputs
This command will watch your project for changes and automatically regenerate the necessary files.
Step 4: Integrate the BLoC with the UI
Finally, we’ll connect our BLoC to the Flutter UI to react to state changes.
In your main widget, for example, main.dart
, you can access the BLoC through 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
: This widget provides theNetworkInfoBloc
instance to the widget tree, making it accessible to any child widget viaBlocProvider.of<NetworkInfoBloc>(context)
.networkInfoBloc.stream.listen(...)
: IninitState
, we subscribe to the BLoC’s state stream. Each time a new state is emitted (which happens when the network status changes), our listener is triggered, and we can display a toast notification.BlocBuilder
: This widget is used to rebuild the UI in response to state changes. It listens for new states from theNetworkInfoBloc
and rebuilds itsbuilder
function, updating theText
widget to reflect the current network status.
Step 5: Display Toast Notifications
The fluttertoast
package provides a simple, platform-agnostic way to show non-intrusive messages.
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,
);
}
This helper function simplifies the process of showing toasts by allowing you to specify a message and a status, which determines the background color.
Conclusion
By combining the power of the BLoC pattern, dependency injection with get_it
and injectable
, and robust network-checking libraries, you can build a highly reliable and maintainable network connectivity checker in your Flutter application.
This architecture ensures your app is responsive to network changes and provides a clean separation of concerns, making your codebase scalable and easy to test.
References
Here are some references that support the concepts and packages used in the article:
Flutter and Dart Fundamentals:
Flutter Official Documentation: Provides comprehensive guides on Flutter development, including widgets, state management, and asynchronous programming.
Dart Official Documentation: Details Dart language features, including asynchronous programming with
Future
andStream
.
Connectivity and Network Checking:
connectivity_plus
package on Pub.dev: Official documentation for theconnectivity_plus
plugin, explaining its usage for checking network connectivity types.internet_connection_checker
package on Flutter Gems: Details theinternet_connection_checker
package, which verifies actual internet access by pinging external servers.http
package on Pub.dev: The official documentation for making HTTP requests in Dart and Flutter.
Dependency Injection:
get_it
package on Pub.dev: The official documentation forget_it
, a simple service locator for Dart and Flutter.injectable
package on Pub.dev: The official documentation forinjectable
, a code generator forget_it
that simplifies dependency registration.State Management (BLoC):
flutter_bloc
package on Pub.dev – the official documentation for theflutter_bloc
package, providing widgets and utilities for implementing the BLoC pattern.
Immutability and Code Generation:
freezed
package on Pub.dev: The official documentation forfreezed
, a powerful code generator for creating immutable data classes.build_runner
package on Pub.dev: The tool used to run code generators likeinjectable_generator
andfreezed
.
Reactive Programming (RxDart) and Streams:
rxdart
package on Pub.dev: Official documentation for RxDart, which extends Dart’s Stream API with powerful operators likedebounceTime
.“Stream class – dart:async library” on Flutter API Docs: The official Dart documentation for the
Stream
class.
User Feedback:
fluttertoast
package on Pub.dev: Official documentation for thefluttertoast
package for displaying toast messages.
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ