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.
This can be particularly useful for scenarios like saving product catalogs or user preferences.
1. Install ObjectBox
First, add the necessary dependencies in your 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: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,
});
}
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.
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;
}
}