Data Sources (Offline)

An offline data source in mobile app development refers to a local storage system that allows the app to store and retrieve data without requiring a constant internet connection.
This ensures that users can access essential features and information even when offline.
In Flutter, popular solutions for offline data sources include databases like ObjectBox and Isar, which efficiently manage and persist data locally on the device.
These local databases allow seamless data synchronization, caching, and offline-first functionality.

ObjectBox: Offline Data Source

ObjectBox a high-performance database for Flutter and Dart.
It is designed for speed, making it suitable for applications that need real-time, offline-first data storage. ObjectBox offers advantages like ACID-compliant transactions,
built-in support for entities, and automatic synchronization, which makes it an ideal choice for mobile apps, as it provides:
  • Speed: ObjectBox is optimized for fast CRUD (Create, Read, Update, Delete) operations.
  • Scalability: ObjectBox supports large-scale data sets and ensures efficiency in dealing with complex relationships between objects.
  • Automatic Sync: ObjectBox offers optional synchronization, allowing data to be synced automatically between devices or servers when connected.
By using ObjectBox as an offline data source, you can cache data locally, ensuring that users can continue to interact with your app even when they are offline.
This can be particularly useful for scenarios like saving product catalogs or user preferences.
Offline

1. Install ObjectBox

First, add the necessary dependencies in your pubspec.yaml:
  • pubspec.yaml
dependencies:
  path_provider: ^2.1.1
  objectbox: ^4.0.3
  objectbox_flutter_libs: ^4.0.3

dev_dependencies:
  build_runner: ^2.4.11
  objectbox_generator: ^4.0.3

objectbox:
  # Writes objectbox-model.json and objectbox.g.dart to lib/{custom}
  output_dir: common/data/database

2. Define Objectbox Model

When defining the LocalProduct model in the context of using Objectbox as a local database for Flutter:
  • local_product.dart
  • product.dart
import 'package:objectbox/objectbox.dart';

@entity
class LocalProduct {
  /// The @Id annotation marks the primary key of the entity in [ObjectBox].
  /// This field is used to uniquely identify each entity object in the database.
  /// In our case, the id field is an integer, and [ObjectBox] automatically manages its value when inserting new records.
  /// If you set id to 0 when creating an object, ObjectBox assigns a unique value upon insertion.
  @Id()
  int id;
  
  /// The @Unique annotation ensures that the value in the productId field is unique across all entities.
  /// If a conflict occurs (for example, if you attempt to insert another product with the same productId),
  /// [ObjectBox] resolves it based on the specified onConflict strategy. In our case, ConflictStrategy.replace is used,
  /// meaning that if a duplicate productId is found, the existing entry is replaced with the new one.
  @Unique(onConflict: ConflictStrategy.replace)
  int? productId;

  String? name;
  String? description;

  /// objectbox doesn't support [num] -> convert [price] to [double]
  double? price;

  LocalProduct({
    this.id = 0,
    this.productId,
    this.name,
    this.description,
  });
}
After defining the model, run the following command to generate the necessary Objectbox code:
dart run build_runner build --delete-conflicting-outputs

3. Define Model Converters

In order to manage data from both local (Objectbox) and remote (Retrofit) data-sources, we need to create converters to map between the LocalEntity and Entity models.
This will allow smooth data handling between local and remote sources within the repository.
  • local_product.dart
class LocalProduct {
  // ...
  LocalProduct({
    // ...
  });

  /// convert [Product] to [LocalProduct]
  factory LocalProduct.fromEntity(Product product) {
    return LocalProduct(
      productId: product.id,
      name: product.name,
      description: product.description,
      price: product.price?.toDouble(),
    );
  }

  /// convert [LocalProduct] to [Product]
  Product fromLocal() => Product(
    id: productId,
    name: name,
    description: description,
    price: price,
  );
}

4. Define Local-Product Data-Source

When creating a local data source for Objectbox, the main responsibility of this layer is performing database operations by managing CRUD (Create, Read, Update, Delete) operations for your data models—in this case, the LocalProduct model.
Below is a step-by-step explanation of how to define an Objectbox local data source for managing LocalProduct instances.
import 'objectbox.g.dart'; // generated objectbox file
import 'local_product.dart';

class ProductsLocalDataSource {
  /// find [LocalProduct] from local-database
  Future<LocalProduct?> findOne(int id) async {
    final store = await openStore();
    final box = store.box<LocalProduct>();

    // query builder for find item with filters
    final query = box.query().build();
    query.param(LocalProduct_.productId).value = id;

    // fetch item
    final item = query.findFirst();

    // clean up resources
    query.close();
    store.close();

    return item;
  }

  /// fetch cached [LocalProduct] list from local-database
  Future<List<LocalProduct>> findAll() async {
    final store = await openStore();
    final box = store.box<LocalProduct>();

    // query builder for find items with filters
    final query = box.query().build();
    
    // fetch items
    final items = query.find();
    
    // clean up resources
    query.close();
    store.close();

    return items;
  }

  /// save list of [LocalProduct] in local-database
  Future<void> insertAll(List<LocalProduct> data) async {
    final store = await openStore();
    final box = store.box<LocalProduct>();

    // save items
    box.putMany(data);
    
    // clean up resources
    store.close();
  }

  /// delete [LocalProduct] from local-database
  Future<int> delete(int id) async {
    final store = await openStore();
    final box = store.box<LocalProduct>();

    // find item
    final query = box.query(LocalProduct_.productId.equals(id)).build();

    // remove item
    final result = query.remove();

    // clean up resources
    query.close();
    store.close();

    return result;
  }

  /// count all documents in local-database
  Future<int> count() async {
    final store = await openStore();
    final box = store.box<LocalProduct>();
    final query = box.query().build();

    // count items
    final count = query.count();
    
    // clean up resources
    query.close();
    store.close();

    return count;
  }
}

5. Implementing Objectbox

We will discuss this in Repositories section.