flutter-working-with-databases
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseArchitecting the Data Layer
数据层架构设计
Contents
目录
Core Architecture
核心架构
Construct the data layer as the Single Source of Truth (SSOT) for all application data. In an MVVM architecture, the data layer represents the Model. Never update application data outside of this layer.
Separate the data layer into two distinct components: Repositories and Services.
将数据层构建为所有应用数据的单一可信源(Single Source of Truth,SSOT)。在MVVM架构中,数据层代表Model。绝不要在该层之外更新应用数据。
将数据层拆分为两个独立组件:仓库(Repositories) 和 服务(Services)。
Repositories
仓库(Repositories)
- Act as the SSOT for a specific domain entity.
- Contain business logic for data mutation, polling, caching, and offline synchronization.
- Transform raw data models (API/DB models) into Domain Models (clean data classes containing only what the UI needs).
- Inject Services as private members to prevent the UI layer from bypassing the repository.
- 作为特定领域实体的单一可信源。
- 包含数据变更、轮询、缓存和离线同步的业务逻辑。
- 将原始数据模型(API/数据库模型)转换为领域模型(Domain Models,仅包含UI所需数据的干净数据类)。
- 以私有成员的方式注入服务,防止UI层绕过仓库直接访问服务。
Services
服务(Services)
- Act as stateless wrappers around external data sources (HTTP clients, SQLite databases, platform plugins).
- Perform no business logic or data transformation beyond basic JSON serialization.
- Return raw data models or wrappers to the calling repository.
Result
- 作为外部数据源(HTTP客户端、SQLite数据库、平台插件)的无状态包装器。
- 除基础JSON序列化外,不执行任何业务逻辑或数据转换。
- 向调用的仓库返回原始数据模型或包装对象。
Result
Services Implementation
服务实现
Database Services (SQLite)
数据库服务(SQLite)
Use databases to persist and query large amounts of structured data locally.
- Add and
sqflitepackages topath.pubspec.yaml - Use the package to define the storage location on disk safely across platforms.
path - Define table schemas using constants to prevent typos.
- Use as the primary key with
idto improve query and update times.AUTOINCREMENT - Always use in SQL queries to prevent SQL injection (e.g.,
whereArgs).where: 'id = ?', whereArgs: [id]
使用数据库在本地持久化和查询大量结构化数据。
- 向中添加
pubspec.yaml和sqflite包。path - 使用包跨平台安全定义磁盘上的存储位置。
path - 使用常量定义表结构,防止拼写错误。
- 使用作为主键并开启
id,以提升查询和更新效率。AUTOINCREMENT - 在SQL查询中始终使用防止SQL注入(例如:
whereArgs)。where: 'id = ?', whereArgs: [id]
API Services
API服务
- Wrap HTTP calls (e.g., using the package) in dedicated client classes.
http - Return asynchronous response objects (or
Future).Stream - Handle raw JSON serialization at this level, returning API-specific data models.
- 将HTTP调用(例如使用包)封装在专用客户端类中。
http - 返回异步响应对象(或
Future)。Stream - 在该层处理原始JSON序列化,返回特定于API的数据模型。
Repository Implementation
仓库实现
Domain Models
领域模型(Domain Models)
- Define immutable data classes (using or
freezed) for Domain Models.built_value - Strip out backend-specific fields (like metadata or pagination tokens) that the UI does not need.
- 为领域模型定义不可变数据类(使用或
freezed)。built_value - 剔除UI不需要的后端特定字段(如元数据或分页令牌)。
Offline-First Synchronization
离线优先同步
Combine local and remote data sources within the repository to provide seamless offline support.
- If reading data: Return a that immediately yields the cached local data from the Database Service, performs the network request via the API Service, updates the Database Service, and then yields the fresh data.
Stream - If writing data (Online-only): Attempt the API Service mutation first. If successful, update the Database Service.
- If writing data (Offline-first): Update the Database Service immediately. Attempt the API Service mutation. If the network fails, flag the local database record as and queue a background synchronization task.
synchronized: false
在仓库中结合本地和远程数据源,提供无缝的离线支持。
- 读取数据时: 返回一个,立即从数据库服务返回缓存的本地数据,通过API服务执行网络请求,更新数据库服务,然后返回最新数据。
Stream - 写入数据(仅在线): 先尝试通过API服务执行变更。如果成功,再更新数据库服务。
- 写入数据(离线优先): 立即更新数据库服务。尝试通过API服务执行变更。如果网络请求失败,将本地数据库记录标记为,并将其加入后台同步任务队列。
synchronized: false
Caching Strategies
缓存策略
Select the appropriate caching strategy based on the data payload:
- Small Key-Value Data: Use for simple app configurations, theme settings, or user preferences.
shared_preferences - Large Datasets: Use relational (,
sqflite) or non-relational (drift,hive_ce) on-device databases.isar_community - Images: Use the package to automatically cache remote images to the device's file system.
cached_network_image - API Responses: Implement lightweight remote caching within the API Service or Repository using in-memory maps or temporary file storage.
根据数据负载选择合适的缓存策略:
- 小型键值数据: 使用存储简单的应用配置、主题设置或用户偏好。
shared_preferences - 大型数据集: 使用关系型(、
sqflite)或非关系型(drift、hive_ce)设备端数据库。isar_community - 图片: 使用包自动将远程图片缓存到设备的文件系统。
cached_network_image - API响应: 在API服务或仓库中使用内存映射或临时文件存储实现轻量级远程缓存。
Workflows
工作流程
Workflow: Implementing a New Data Feature
工作流程:实现新的数据功能
Copy and track this checklist when adding a new data entity to the application.
- Task Progress
- Define the Domain Model (immutable, UI-focused).
- Define the API/DB Models (raw data structures).
- Create or update the Service(s) to handle raw data fetching/storage.
- Create the Repository interface (abstract class).
- Implement the Repository, injecting the required Service(s) as private dependencies.
- Map raw Service models to the Domain Model within the Repository.
- Expose Repository methods to the View Model.
- Run validator -> review errors -> fix.
添加新数据实体到应用时,可参考并跟踪以下检查清单。
- 任务进度
- 定义领域模型(不可变、面向UI)。
- 定义API/数据库模型(原始数据结构)。
- 创建或更新服务以处理原始数据的获取/存储。
- 创建仓库接口(抽象类)。
- 实现仓库,将所需服务作为私有依赖注入。
- 在仓库中将原始服务模型映射为领域模型。
- 向ViewModel暴露仓库方法。
- 运行验证器 -> 查看错误 -> 修复。
Workflow: Implementing SQLite Persistence
工作流程:实现SQLite持久化
Follow this sequence to add a new SQLite table and integrate it.
- Task Progress
- Add and
sqflitedependencies.path - Define table name and column constants.
- Update the or
onCreatemethod in the Database Service to execute theonUpgradestatement.CREATE TABLE - Implement ,
insert,query, andupdatemethods in the Database Service.delete - Inject the Database Service into the target Repository.
- Ensure the Repository calls before executing queries.
database.open()
- Add
按照以下步骤添加新的SQLite表并完成集成。
- 任务进度
- 添加和
sqflite依赖。path - 定义表名和列常量。
- 在数据库服务的或
onCreate方法中更新执行onUpgrade语句。CREATE TABLE - 在数据库服务中实现、
insert、query和update方法。delete - 将数据库服务注入目标仓库。
- 确保仓库在执行查询前调用。
database.open()
- 添加
Examples
示例
Offline-First Repository Implementation
离线优先仓库实现
This example demonstrates a Repository coordinating between a Database Service and an API Service using a Stream for offline-first reads.
dart
import 'dart:async';
class TodoRepository {
TodoRepository({
required DatabaseService databaseService,
required ApiClientService apiClientService,
}) : _databaseService = databaseService,
_apiClientService = apiClientService;
final DatabaseService _databaseService;
final ApiClientService _apiClientService;
/// Yields local data immediately, then fetches remote data, updates local, and yields fresh data.
Stream<List<Todo>> observeTodos() async* {
// 1. Yield local cached data first
final localTodos = await _databaseService.getAllTodos();
if (localTodos.isNotEmpty) {
yield localTodos.map((model) => Todo.fromDbModel(model)).toList();
}
try {
// 2. Fetch fresh data from API
final remoteTodos = await _apiClientService.fetchTodos();
// 3. Update local database
await _databaseService.replaceAllTodos(remoteTodos);
// 4. Yield fresh data
yield remoteTodos.map((model) => Todo.fromApiModel(model)).toList();
} on Exception catch (e) {
// Handle network errors (UI will still have local data)
// Log error or yield a specific error state if required
}
}
/// Offline-first write: Save locally, then attempt remote sync.
Future<void> createTodo(Todo todo) async {
final dbModel = todo.toDbModel().copyWith(isSynced: false);
// 1. Save locally immediately
await _databaseService.insertTodo(dbModel);
try {
// 2. Attempt remote sync
final apiModel = await _apiClientService.postTodo(todo.toApiModel());
// 3. Mark as synced locally
await _databaseService.updateTodo(
dbModel.copyWith(id: apiModel.id, isSynced: true)
);
} on Exception catch (_) {
// Leave as isSynced: false for background sync task to pick up later
}
}
}此示例展示了一个仓库如何协调数据库服务和API服务,使用Stream实现离线优先读取。
dart
import 'dart:async';
class TodoRepository {
TodoRepository({
required DatabaseService databaseService,
required ApiClientService apiClientService,
}) : _databaseService = databaseService,
_apiClientService = apiClientService;
final DatabaseService _databaseService;
final ApiClientService _apiClientService;
/// Yields local data immediately, then fetches remote data, updates local, and yields fresh data.
Stream<List<Todo>> observeTodos() async* {
// 1. Yield local cached data first
final localTodos = await _databaseService.getAllTodos();
if (localTodos.isNotEmpty) {
yield localTodos.map((model) => Todo.fromDbModel(model)).toList();
}
try {
// 2. Fetch fresh data from API
final remoteTodos = await _apiClientService.fetchTodos();
// 3. Update local database
await _databaseService.replaceAllTodos(remoteTodos);
// 4. Yield fresh data
yield remoteTodos.map((model) => Todo.fromApiModel(model)).toList();
} on Exception catch (e) {
// Handle network errors (UI will still have local data)
// Log error or yield a specific error state if required
}
}
/// Offline-first write: Save locally, then attempt remote sync.
Future<void> createTodo(Todo todo) async {
final dbModel = todo.toDbModel().copyWith(isSynced: false);
// 1. Save locally immediately
await _databaseService.insertTodo(dbModel);
try {
// 2. Attempt remote sync
final apiModel = await _apiClientService.postTodo(todo.toApiModel());
// 3. Mark as synced locally
await _databaseService.updateTodo(
dbModel.copyWith(id: apiModel.id, isSynced: true)
);
} on Exception catch (_) {
// Leave as isSynced: false for background sync task to pick up later
}
}
}SQLite Database Service Implementation
SQLite数据库服务实现
Demonstrates safe query construction using .
whereArgsdart
class DatabaseService {
static const String _tableName = 'todos';
static const String _colId = 'id';
static const String _colTask = 'task';
static const String _colIsSynced = 'is_synced';
Database? _database;
Future<void> open() async {
if (_database != null) return;
final dbPath = join(await getDatabasesPath(), 'app_database.db');
_database = await openDatabase(
dbPath,
version: 1,
onCreate: (db, version) {
return db.execute(
'CREATE TABLE $_tableName('
'$_colId INTEGER PRIMARY KEY AUTOINCREMENT, '
'$_colTask TEXT, '
'$_colIsSynced INTEGER)'
);
},
);
}
Future<void> updateTodo(TodoDbModel todo) async {
await _database!.update(
_tableName,
todo.toMap(),
where: '$_colId = ?',
whereArgs: [todo.id], // Prevents SQL injection
);
}
}展示如何使用安全构建查询。
whereArgsdart
class DatabaseService {
static const String _tableName = 'todos';
static const String _colId = 'id';
static const String _colTask = 'task';
static const String _colIsSynced = 'is_synced';
Database? _database;
Future<void> open() async {
if (_database != null) return;
final dbPath = join(await getDatabasesPath(), 'app_database.db');
_database = await openDatabase(
dbPath,
version: 1,
onCreate: (db, version) {
return db.execute(
'CREATE TABLE $_tableName('
'$_colId INTEGER PRIMARY KEY AUTOINCREMENT, '
'$_colTask TEXT, '
'$_colIsSynced INTEGER)'
);
},
);
}
Future<void> updateTodo(TodoDbModel todo) async {
await _database!.update(
_tableName,
todo.toMap(),
where: '$_colId = ?',
whereArgs: [todo.id], // Prevents SQL injection
);
}
}