Providing offline capabilities to your Flutter app using NetworkBoundResource

Gaganpreet Singh
Geek Culture
Published in
5 min readJul 28, 2021

Background

It's been a while since I wrote the last article. This article is an extension of the previous article where I had talked about Clean Architecture for Enterprise Flutter applications. To begin with this article, I would highly recommend going and read that article first. It will be easy for you guys to understand the application design, plugins, and terms that I will be using in this article from the last one.

Offline Capability — Introduction

People use mobile apps on the move. As mobile networks are frequently flaky, if our app did not have the ability to work well offline, every time an app user took a subway or a plane, or an Uber our app will potentially lose connectivity leading to a frustrating user experience.

Sometimes, showing a blank screen in case of network error is not just enough. For a great user experience, we should always present data to users. So, in case of no or poor network connectivity, we need to show the old content to the user while waiting to get new data from the network and once new data is available, it should be cached in the database. This new cached data now should be available to the user.

The most important point to understand is, the database is only the single source of truth. Whatever data you will get from the server, will first be cached in the database and UI will be driven from the database. So by anyway if your database or data tables get changes, your UI will be notified and updated.

NetworkBoundResource— what it is and what it does

If you are coming from the Android app development world, you might be familiar with this term. The Android Jetpack article, Guide to App Architecture, describes an algorithm for providing data to an app by either retrieving sufficiently recent data from a local cache or loading the latest data from the network.

The original NetworkBoundResource algorithm

Following the NetworkBoundResource algorithm, I’ve written similar code to provide offline capabilities in Flutter applications. If you are interested to look into the source code of the original NetworkBoundResource class then you can check Google’s Github sample.

My approach to NetworkBoundResource is slightly different from the original idea. Instead of using it as a class in the repository layer, I’ve defined it as a function (getNetworkBoundData()) in the BaseRepository class that will be accessible to all the repositories. Based on your requirement, you can create separate code from BaseRepository and put it in some other repository (e.g. BaseNetworkBoundRepository) so that only required repositories should extend the offline functionality.

Note: For the sample project, I am using Retrofit (with Dio) to make network calls and Floor to save and retrieve data to and from the local database.

Implementation and usage

The sample project is using the following common terms:

  • Dto — This is used to represent the data received from the server
  • Entity — This is used to represent the data stored/retrieved to/from the database
  • Model — This is used to represent the data that will be shown on the UI

Below is the code snippet of input and output of the getNetworkBoundData method

Stream<Resource<Model?>> getNetworkBoundData<Dto, Entity, Model>(
{required LoadFromDb<Entity> loadFromDb,
required CreateNetworkCall<Dto> createNetworkCall,
required EntityToModelMap<Entity, Model> map,
required SaveNetworkResult<Dto> saveNetworkResult,
ShouldRefresh<Entity>? shouldRefresh,
OnNetworkCallFailure? onNetworkCallFailure}) async* {
...
...
...
}

If you took a close look at the code snippet above, you should have noticed that using this method only requires us to provide 4 functions:

  • loadFromDb: Future<Entity?> Function() — This method loads the data saved locally in Database
  • createNetworkCall: Future<Dto?> Function() — Here we will call a function that will fetch data from the server
  • map: Model? Function(Entity? entity) — Here we will map the data retrieved from the Database to the model class that will be returned to the caller
  • saveNetworkResult: Future<void> Function(Dto? dto) — This is where we will use DAO to save locally whatever data we just fetched from the server

There are two other optional parameters:

  • shouldRefresh: bool Function(Entity? entity) — Based on the data retrieved from the database, this determines whether the app should fetch new data from the server and update the data already stored on our local device database.
  • onNetworkCallFailure: Function(Exception) — This is where we will log the network failure details or schedule the network call again

To provide the offline capability in your Flutter application, you only need to call this function and provide the required functions. Below is the code snippet from the sample project to cache the articles and show them before making the network call

Stream<Resource<List<ArticleModel>?>> getArticles(bool forceRefresh) {
/// get always from network in case of force refresh,
/// otherwise use cached approach to load data
if (forceRefresh)
return getNetworkData<ArticleResponse, List<ArticleModel>>(
createNetworkCall: () => _articleService.getArticles(),
map: (response) =>
response?.articles.map((e) => e.toModel()).toList());
else
return getNetworkBoundData<ArticleResponse, List<ArticleEntity>,
List<ArticleModel>>(
loadFromDb: () => _articleDao.getArticles(),
createNetworkCall: () => _articleService.getArticles(),
map: (list) => list?.map((e) => e.toModel()).toList(),
saveNetworkResult: (response) async {
if (response != null) {
await _articleDao.saveArticles(
response.articles.map((e) => e.toEntity()).toList());
}
},
onNetworkCallFailure: (ex) => {print('Network call failed: $ex')});
}

If you took a close look at the code snippet above, you should have noticed that we are making two function calls based on the forceRefresh parameter. In case of force refresh, we are always making the network call and returning the data to the caller. But in case of no force refresh, we are using the cached approach to show the cached articles first and get the new articles from the network.

Resource — what it is and what it does

Resource is a generic class that holds data and its state. There can be following 4 states:

  • LOADING — This indicates data is being loaded
  • SUCCESS — This indicates data has been loaded successfully
  • FAILURE — This indicates that data failure occurred while fetching data from the network
  • EXCEPTION — This indicates that an exception occurred while fetching data from the network

Each of these states holds the data information that can be used either to show error messages to the user or for logging purposes. This class will be used along with getNetworkBoundData() to get the data.

NetworkBoundWidget

As getNetworkBoundData() returns a Stream of data, you can simply use the StreamBuilder widget and build the widgets based on the state of the resource. To simplify things, I have created NetworkBoundWidget on top of the StreamBuilder widget to listen for the states and build the widgets accordingly.

Widget _buildBody(BuildContext context, ArticleListViewModel viewModel) {
return NetworkBoundWidget<List<ArticleModel>>(
stream: viewModel.articlesStream,
child: (context, data) => _buildListView(data, viewModel));
}

As you can see, you only need to provide a stream reference to get the data and a child widget to show the data. You can also provide widgets for other states like loading, failure, and exception as per your requirements. By default, it shows a loader and error details for these states.

Final thoughts

NetworkBoundResource algorithm is just an approach that simplifies the data caching logic and helps to write an offline-first app easily. You can use your choice of database and network client, it doesn’t bound you to stick with the options used in the sample project.

--

--

Gaganpreet Singh
Geek Culture

Tech enthusiast, Consultant, Android and Flutter developer