flutter-working-with-databases

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Architecting 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
    Result
    wrappers to the calling repository.
  • 作为外部数据源(HTTP客户端、SQLite数据库、平台插件)的无状态包装器。
  • 除基础JSON序列化外,不执行任何业务逻辑或数据转换。
  • 向调用的仓库返回原始数据模型或
    Result
    包装对象。

Services Implementation

服务实现

Database Services (SQLite)

数据库服务(SQLite)

Use databases to persist and query large amounts of structured data locally.
  • Add
    sqflite
    and
    path
    packages to
    pubspec.yaml
    .
  • Use the
    path
    package to define the storage location on disk safely across platforms.
  • Define table schemas using constants to prevent typos.
  • Use
    id
    as the primary key with
    AUTOINCREMENT
    to improve query and update times.
  • Always use
    whereArgs
    in SQL queries to prevent SQL injection (e.g.,
    where: 'id = ?', whereArgs: [id]
    ).
使用数据库在本地持久化和查询大量结构化数据。
  • pubspec.yaml
    中添加
    sqflite
    path
    包。
  • 使用
    path
    包跨平台安全定义磁盘上的存储位置。
  • 使用常量定义表结构,防止拼写错误。
  • 使用
    id
    作为主键并开启
    AUTOINCREMENT
    ,以提升查询和更新效率。
  • 在SQL查询中始终使用
    whereArgs
    防止SQL注入(例如:
    where: 'id = ?', whereArgs: [id]
    )。

API Services

API服务

  • Wrap HTTP calls (e.g., using the
    http
    package) in dedicated client classes.
  • Return asynchronous response objects (
    Future
    or
    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
    freezed
    or
    built_value
    ) for Domain Models.
  • 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
    Stream
    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.
  • 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
    synchronized: false
    and queue a background synchronization task.
在仓库中结合本地和远程数据源,提供无缝的离线支持。
  • 读取数据时: 返回一个
    Stream
    ,立即从数据库服务返回缓存的本地数据,通过API服务执行网络请求,更新数据库服务,然后返回最新数据。
  • 写入数据(仅在线): 先尝试通过API服务执行变更。如果成功,再更新数据库服务。
  • 写入数据(离线优先): 立即更新数据库服务。尝试通过API服务执行变更。如果网络请求失败,将本地数据库记录标记为
    synchronized: false
    ,并将其加入后台同步任务队列。

Caching Strategies

缓存策略

Select the appropriate caching strategy based on the data payload:
  • Small Key-Value Data: Use
    shared_preferences
    for simple app configurations, theme settings, or user preferences.
  • Large Datasets: Use relational (
    sqflite
    ,
    drift
    ) or non-relational (
    hive_ce
    ,
    isar_community
    ) on-device databases.
  • Images: Use the
    cached_network_image
    package to automatically cache remote images to the device's file system.
  • 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
      sqflite
      and
      path
      dependencies.
    • Define table name and column constants.
    • Update the
      onCreate
      or
      onUpgrade
      method in the Database Service to execute the
      CREATE TABLE
      statement.
    • Implement
      insert
      ,
      query
      ,
      update
      , and
      delete
      methods in the Database Service.
    • Inject the Database Service into the target Repository.
    • Ensure the Repository calls
      database.open()
      before executing queries.
按照以下步骤添加新的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
whereArgs
.
dart
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
    );
  }
}
展示如何使用
whereArgs
安全构建查询。
dart
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
    );
  }
}