Offline-First Applications with Flutter
- Published on

Implementing an offline-first application with Flutter as the front-end requires a thoughtful approach to data synchronization, caching, and state management. The goal is to enable the app to function effectively without an internet connection, using local storage, and then synchronize any changes with the server once the connection is restored.
- Offline-First Architecture
- Set Up Connectivity Monitoring
- Implement Offline Data Saving
- Implement Synchronization Logic
- Handle Form Submission Based on Connectivity
Offline-First Architecture
Offline-first architecture is a design approach for mobile applications (and other software) that prioritizes the app's functionality and performance in offline scenarios. This means that the app is built to function without a constant internet connection, ensuring that users can access core features and data even when they're not connected to the web.
Key characteristics of offline-first architecture include:
Data Caching: The app stores relevant data locally on the user's device so that it can be accessed without an internet connection. Data might include user preferences, application state, or content that has been previously downloaded.
Synchronization: When a connection is available, the app synchronizes local changes with a remote server. This ensures consistency between the data on the device and any backend systems.
Conflict Resolution: The architecture often includes mechanisms to handle conflicts that may arise when changes are made to the same data from different devices or when offline changes are synced back to the server after a period of being offline.
User Experience: Offline-first apps are designed with a seamless user experience in mind, minimizing disruptions due to connectivity issues and providing users with immediate feedback even when actions cannot be instantly synced with a server.
Background Operations: Many offline-first apps perform background tasks such as pre-fetching data or updating content whenever a connection is available so that the latest information is ready for use at any time.
By adopting an offline-first approach, developers aim to create robust mobile applications that deliver consistent performance and usability regardless of network conditions, which can be particularly beneficial in areas with poor connectivity or for users who are frequently on-the-move.
Set Up Your Flutter Project
- Create a new Flutter project using the command
flutter create offline_first
. - Add the necessary dependencies in your
pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
connectivity_plus: ^5.0.2
hive: ^2.2.3
hive_flutter: ^1.1.0
http: ^1.2.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
build_runner: ^2.4.8
hive_generator: ^2.0.1
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
- Run
flutter packages get
to install the dependencies.
Configure Hive for Local Storage
To Initialize Hive and register a custom adapter FormDataAdapter
in your Flutter app, you can follow these steps. Let's assume you have a model named FormData
that you want to store using Hive.
Create your model class FormData
:
import 'package:hive/hive.dart';
part 'local_storage.g.dart';
(typeId: 0)
class FormData extends HiveObject {
(0)
late String field1;
(1)
late String field2;
(2)
late String field3;
}
After creating your model, run the build runner command to generate the adapter:
flutter pub run build_runner build
This will generate a file named form_data.g.dart
with your FormDataAdapter
.
Now, let's set up Hive and register the adapter in the main()
function of your app:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'local_storage.dart';
import 'package:http/http.dart' as http;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive and register the adapter.
await Hive.initFlutter();
Hive.registerAdapter(FormDataAdapter());
runApp(const MyApp());
}
This will initialize Hive with the necessary adapters before running your app and open a box ready for storing instances of FormData
. Make sure that any interaction with the database occurs after initializing it in the main function.
Remember to call Hive.close()
when your app is being terminated to properly dispose of any resources used by Hive.
Set Up Connectivity Monitoring
Using the connectivity_plus
package, set up a stream listener in your main app state class _MyHomePageState
to monitor connectivity changes, which will be used to update UI and trigger synchronization. Give me a sample code
To use the connectivity_plus
package in your Flutter app, you first need to add it to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
connectivity_plus: ^5.0.3
...
After adding the dependency, run flutter pub get
to install it.
Next, you can implement a stream listener for connectivity changes in your _MyHomePageState
class. Below is an example code that demonstrates how to do this:
final Connectivity _connectivity = Connectivity();
late Stream<ConnectivityResult> _connectivityStream;
ConnectivityResult _connectionStatus = ConnectivityResult.none;
This sample code sets up a listener that listens for changes in the device's network connectivity and updates the _connectionStatus
variable. The _updateConnectionStatus
method is called whenever there is a change in connectivity, where you can also trigger any desired actions like synchronizing data when there's an active connection.
void _updateConnectionStatus(
ConnectivityResult result, BuildContext context) async {
setState(() => _connectionStatus = result);
if (result != ConnectivityResult.none && formDataBox != null) {
final unsyncedData = formDataBox!.values.toList();
for (var formData in unsyncedData) {
bool success = await sendDataToBackend(formData);
if (success) {
await formData
.delete(); // Remove from local storage after successful sync.
}
}
}
}
Implement Offline Data Saving
When there's no internet connection, use Hive to save form data locally by adding it into a Box named 'formData'
. The method saveDataLocally()
is responsible for this operation.
void saveDataLocally() {
final formData = FormData()
..field1 = _field1Controller.text
..field2 = _field2Controller.text
..field3 = _field3Controller.text;
formDataBox?.add(formData);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Data saved locally!')),
);
clearFields();
}
Implement Synchronization Logic
Create a method called syncDataWithBackend()
which iterates over all saved local entries and attempts to send them to your backend server through an HTTP POST request (sendDataToBackend()
). Successful synchronization should remove data from local storage.
// Sync data with backend
Future<void> syncDataWithBackend() async {
if (_isSyncing) return; // Prevent multiple sync attempts simultaneously
if (_connectionStatus != ConnectivityResult.none) {
setState(() {
_isSyncing = true;
});
final unsyncedData = formDataBox?.values.toList() ?? [];
for (var data in unsyncedData) {
await sendDataToBackend(data);
await data.delete(); // Remove from local storage after successful sync.
}
setState(() {
_isSyncing = false;
});
if (unsyncedData.isNotEmpty) {
clearFields(); // Clear fields only if there was something to sync
}
}
}
// Send data to backend
Future<bool> sendDataToBackend(FormData data) async {
// Simulate a delay for testing purposes
await Future.delayed(const Duration(seconds: 2));
try {
var response = await http.post(
Uri.parse('https://[BACKEND_API_URL]/data/add'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'field1': data.field1,
'field2': data.field2,
'field3': data.field3,
}),
);
if (response.statusCode == 201) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Data sent to backend!')),
);
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to send data!')),
);
return false;
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to send data!')),
);
return false;
}
}
Handle Form Submission Based on Connectivity
In the button's onPressed callback, check network availability with _connectionStatus
. If online, call handleSubmit()
, which attempts immediate synchronization; if offline, invoke saveDataLocally()
.
Auto-Sync When Connection Restores
Update _updateConnectionStatus()
method so that whenever connectivity is restored, it automatically calls syncDataWithBackend()
to synchronize any unsynced local data with the backend server.
Test Your Application
First you can find the full source code in my GitHub Repository : https://github.com/claziosi/offline-first
It is time now to test the application... starting from an offline mode, we will save multiple entries locally... when the connectivity will come back, we will synchronize each entry one by one...
