Offline First Approach With Flutter

Tugba Çekirge
CodeBrew
Published in
8 min readJun 14, 2023

Hello!

My name is Tugba; I am acting as a software development team lead at Arkitek R&D. In Arkitek, we are building mobile, desktop, and web applications using technologies such as VueJS, Flutter, and C#. Since 2021, we have been developing our web/mobile projects in Flutter, and needless to say, Flutter is fantastic. After two years, we hived many know-how and some workarounds and I will try to share whenever I find a chance.

If you like my content, you can buy me a pizza!

Before we begin, I would like to express my gratitude to the Flutter Ankara team for extending the invitation to us. We had an amazing time! It was a pleasure to be a part of the event, and we look forward to meeting again in future gatherings. Thank you once again for the opportunity! ❤

What is Offline First?

In this article, I will try to explain these topics:

  1. Offline-first application: I will discuss the concept of an offline-first application, highlighting its benefits and key characteristics. This approach prioritizes offline functionality and data availability, ensuring a seamless user experience even in the absence of an internet connection.
  2. Sqflite: I will explain Sqflite, a popular local database solution for Flutter applications. This section will cover its features, advantages, and suitability for creating relational databases.
  3. Designing our application (Arkitek DORS): This section will provide an overview of how we designed our application, named Arkitek DORS. I will discuss the architectural considerations, such as data synchronization, connection monitoring, and cache management, that enable an effective offline-first approach.

By addressing these three core topics, readers will develop a comprehensive understanding of offline-first applications, Sqflite as a local database solution, and the design principles behind our Arkitek DORS application.

Online vs Offline First Diagram

Offline-First is an approach that ensures seamless functionality of software both offline and online. To achieve this, critical data needs to be stored on the client side, allowing the application to continue operating even without internet connectivity. This is accomplished by utilizing a local database to store data obtained from the original source, such as an API. When the internet connection is restored, the data is then sent to the backend in the background, ensuring synchronization between the local and remote databases.

Offline-first is primarily intended for mobile applications that frequently encounter unstable internet connections, such as in below-ground level areas or rural countryside villages. It is important to understand that offline-first does not solely mean no internet connection. Instead, it focuses on ensuring uninterrupted app functionality by using locally stored data until a stable internet connection is available for synchronization with the backend system.

Packages:

Sqflite:

Sqflite is a Flutter plugin for SQLite, a self-contained, high-reliability, embedded, SQL database engine. Supports iOS, Android and MacOS. Transaction and batch support. It’s very easy to use, especially if you are familiar with SQL. If you want to learn more about sqflite, check out SQFlite In Flutter, an awesome medium article by Raksha Goswami.

Connection Status:

Connection status refers our devices network status, whether it is online(connected to internet) or offline. connectivity_plus plugin allows Flutter apps to discover network connectivity and configure themselves accordingly. It can distinguish between cellular vs Wi-Fi connections. If you like to learn more, check out this awesome medium article, Manage Connectivity In Your Flutter App by Gaspard Merten

Let’s begin:

1- Packages: Add these packages to your pubspec.yaml and save

2- Connection Monitoring: We are going to create a file named connectionStatus.dart, which will include our functions for connection status controls. We are going to use this to check our connection and log our actions. It should look like this:

Important Note: The “GetNotificationCount” endpoint is a vital component of our REST API. You might question why we didn’t opt for a public API or simply ping a service like Google. The reason is our intention extends beyond verifying a general internet connection. We specifically need to ensure that our application can establish communication with our dedicated server. This ensures reliable access to the required data and functionality, enabling a seamless user experience that aligns with our specific needs.

3- Initialize connectionStatus: To initialize connection checker, navigate to the main.dart file and include the following line of code:

void main() async {
connectionStatus.initialize();

When you call the initialize method, it sets up a listener to track changes in your network connection, such as switching from mobile data to Wi-Fi or going offline. This listener triggers the _connectionChange event whenever a connection change occurs. This enables your application to promptly respond and adapt to these changes, ensuring smooth operation and a seamless user experience.

Important Note: To address the issue of setting variables correctly when the device starts without internet connectivity, we made the decision to call the “checkConnection()” method during the initialization process. This ensures proper variable setup even if the device lacks internet access initially, as the connectionChange event may not trigger in such cases. However, it’s important to note that when the device is online, this approach results in two requests to the server during the initialization phase. While this incurs an additional cost, we considered it worthwhile to ensure accurate variable assignment in offline scenarios.

//Hook into flutter_connectivity's Stream to listen for changes
//And check the connection status out of the gate
void initialize() {
_connectivity.onConnectivityChanged.listen(_connectionChange);
checkConnection();
}

At the initiation of our application, the line _connectivity.onConnectivityChanged.listen(_connectionChange) is invoked, setting up a listener that detects changes in connectivity.

void _connectionChange(ConnectivityResult result) async {
print("_connectivity connection changed: $result");
//if ConnectivityResult.none, there's no need to "check"
if (result != ConnectivityResult.none) {
await checkConnection();
} else {
hasConnection = false;
}
}

The checkConnection function plays a crucial role in overseeing and managing the internet connectivity for our application.

4- Creating Database and Tables: We created a class called dbhelper. This class serves as a centralized hub for all database-related operations, such as creating the database and performing CRUD operations (Create, Read, Update, Delete). By organizing these functions within the dbhelperclass, we can easily manage and manipulate the database, ensuring efficient data management in our application.

The following functions serve as illustrative examples that can be adjusted to suit your specific code implementation:

5- Store Data Locally and Try Sending Periodically: In our application, when a user performs an operation, we take a different approach by not immediately sending the data to the server via an API. Instead, we utilize the insert function in our dbhelper to write the data into our local table. To handle the synchronization with the server, we have a function called “synchronizePendingData” which is triggered periodically by a timer. This timer is initialized in the main.dart file, ensuring that any pending data transfers are processed and synchronized with the server at appropriate intervals. (we set it to 3)

Future<bool> synchronizePendingData() async {
bool isTimerRunning = false;
print("synchronizePendingData");
timer = Timer.periodic(Duration(minutes: 3), (timer) async {
pendingData = await checkPendingData();
isTimerRunning = true;
if (pendingData && connectionStatus.hasConnection) {
{
sendTransferObjectToAPI();
}
}
});
return isTimerRunning;
}

6- checkPendingData: This function performs a select query on our tables and filters unsent data using the “ISSENT” column. If the count of unsent data is greater than zero, function returns true and our flow proceeds to the sendTransferObjectToAPI function.

The following functions serve as illustrative examples that can be adjusted to suit your specific code implementation:

Future<bool> checkPendingData() async {
var listLocation =
await dbHelper.getNotSentFromTable('locationServiceTable');
if (listLocation.length > 10) {
return true;
}
var list2 = await dbHelper.getNotSentFromTable('table2');
if (list2.length > 0) {
return true;
}
var list3= await dbHelper.getNotSentFromTable('table3');
if (list3.length > 0) {
return true;
}
}

Within the sendTransferObjectToAPI function, the initial step involves checking for an active internet connection. If the device is online, the function proceeds to send the data to the server via the REST API. After sending the data, we locally update entity.ISSENT columnt to = 1, to prevent sending more than once.

The following functions serve as illustrative examples that can be adjusted to suit your specific code implementation:

Future<void> sendTransferObjectToAPI() async {
if (await connectionStatus.checkConnection()) {
await postLocation();
await postTable1();
await postTable2();
.
.
.
await cacheRefresh();

}
}

Following the successful data transmission, the cacheRefresh function is executed!

The cacheRefresh function performs a series of operations to update the local cache. Firstly, it truncates the relevant tables in the local database, removing existing data. Then, it retrieves fresh data from the server, typically related to items such as an awaiting task list or customer list, and inserts this new data into the local database. The purpose of this process is to ensure an up-to-date and refreshed dataset, enhancing the application’s functionality and responsiveness, particularly when an active internet connection is available.

The following functions serve as illustrative examples that can be adjusted to suit your specific code implementation:

Future<void> cacheRefresh() async {
if (await connectionStatus.checkConnection()) {
await generalService
.getAll(url: "GetMyActiveWorks")
.then((response) async {
if (!response.toString().contains("ArkitekApiErrorOccured")) {
await dbHelper.truncateWorksTable().then((v) async {
await dbHelper.insertWorksTable(response);
});
}
});
      await generalService
.getAll(url: 'GetMyDailyClosedWorks')
.then((response) async {
if (!response.toString().contains("ArkitekApiErrorOccured")) {
await dbHelper.insertWorksTable(response);
}
});
.
.
.
.
}

To successfully implement an offline-first approach in Flutter, follow these key steps:

  1. Local database: Choose a suitable local database solution such as Sqflite, as it supports iOS and Android and allows you to create a relational database. This database will serve as the storage for your application’s data.
  2. Connection monitoring: Set up a connection monitoring mechanism using a stream. By listening to the connection status, you can determine whether your application is online or offline. This enables you to adapt your application’s behavior based on the network availability.
  3. Synchronization timer: Implement a timer that triggers your synchronization function at regular intervals. When the device is online, the synchronization function sends the locally stored data to the server via an API, ensuring that the data stays up to date.
  4. Cache clearing: Include a cache clearing function to delete outdated or unnecessary data from the local database. This ensures that your application does not consume excessive storage space. Additionally, the cache clearing function retrieves updated data from the server, ensuring that your application stays synchronized with the latest information.

By following these steps, you can build a robust offline-first Flutter application that handles data storage, synchronization, and cache management while providing a smooth user experience.

Thank you for reading my article! If you have any questions, you can ask in the comment sections and I will try to clarify.

Have a great day!

--

--

Tugba Çekirge
CodeBrew

Cat mom, metalhead, old gamer, software developer