flutter-databases

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

flutter-data-layer-persistence

Flutter 数据层持久化

Goal

目标

Architects and implements a robust, MVVM-compliant data layer in Flutter applications. Establishes a single source of truth using the Repository pattern, isolates external API and local database interactions into stateless Services, and implements optimal local caching strategies (e.g., SQLite via
sqflite
) based on data requirements. Assumes a pre-configured Flutter environment.
在Flutter应用中架构并实现健壮的、符合MVVM规范的数据层。使用Repository模式建立单一数据源,将外部API和本地数据库交互隔离到无状态Service中,并根据数据需求实现最优的本地缓存策略(例如通过
sqflite
使用SQLite)。假设你已经预先配置好Flutter开发环境。

Decision Logic

决策逻辑

Evaluate the user's data persistence requirements using the following decision tree to select the appropriate caching strategy:
  • Is the data small, simple key-value pairs (e.g., user preferences, theme settings)?
    • Yes: Use
      shared_preferences
      .
  • Is the data a large, structured, relational dataset requiring fast inserts/queries?
    • Yes: Use On-device relational databases (
      sqflite
      or
      drift
      ).
  • Is the data a large, unstructured/non-relational dataset?
    • Yes: Use On-device non-relational databases (
      hive_ce
      or
      isar_community
      ).
  • Is the data primarily API response caching?
    • Yes: Use a lightweight remote caching system or interceptors.
  • Is the data primarily images?
    • Yes: Use
      cached_network_image
      to store images on the file system.
  • Is the data too large for
    shared_preferences
    but doesn't require querying?
    • Yes: Use direct File System I/O.
使用以下决策树评估用户的数据持久化需求,选择合适的缓存策略:
  • 数据是否是小型、简单的键值对(例如用户偏好、主题设置)?
    • 是: 使用
      shared_preferences
  • 数据是否是需要快速插入/查询的大型结构化关系型数据集?
    • 是: 使用设备端关系型数据库(
      sqflite
      或者
      drift
      )。
  • 数据是否是大型非结构化/非关系型数据集?
    • 是: 使用设备端非关系型数据库(
      hive_ce
      或者
      isar_community
      )。
  • 数据是否主要是API响应缓存?
    • 是: 使用轻量级远程缓存系统或者拦截器。
  • 数据是否主要是图片?
    • 是: 使用
      cached_network_image
      将图片存储到文件系统。
  • 数据大小超出
    shared_preferences
    的存储限制但不需要查询能力?
    • 是: 使用直接文件系统I/O。

Instructions

操作指引

  1. Analyze Data Requirements STOP AND ASK THE USER: "What specific data entities need to be managed in the data layer, and what are their persistence requirements (e.g., size, relational complexity, offline-first capabilities)?" Wait for the user's response before proceeding to step 2.
  2. Configure Dependencies Based on the decision logic, add the required dependencies. For a standard SQLite implementation, execute:
    bash
    flutter pub add sqflite path
  3. Define Domain Models Create pure Dart data classes representing the domain models. These models should contain only the information needed by the rest of the app.
    dart
    class Todo {
      final int? id;
      final String title;
      final bool isCompleted;
    
      const Todo({this.id, required this.title, required this.isCompleted});
    
      Map<String, dynamic> toMap() {
        return {
          'id': id,
          'title': title,
          'isCompleted': isCompleted ? 1 : 0,
        };
      }
    
      factory Todo.fromMap(Map<String, dynamic> map) {
        return Todo(
          id: map['id'] as int?,
          title: map['title'] as String,
          isCompleted: map['isCompleted'] == 1,
        );
      }
    }
  4. Implement the Database Service Create a stateless service class to handle direct interactions with the SQLite database.
    dart
    import 'package:path/path.dart';
    import 'package:sqflite/sqflite.dart';
    
    class DatabaseService {
      Database? _database;
    
      Future<void> open() async {
        if (_database != null && _database!.isOpen) return;
        
        _database = await openDatabase(
          join(await getDatabasesPath(), 'app_database.db'),
          onCreate: (db, version) {
            return db.execute(
              'CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, isCompleted INTEGER)',
            );
          },
          version: 1,
        );
      }
    
      bool get isOpen => _database != null && _database!.isOpen;
    
      Future<int> insertTodo(Todo todo) async {
        return await _database!.insert(
          'todos',
          todo.toMap(),
          conflictAlgorithm: ConflictAlgorithm.replace,
        );
      }
    
      Future<List<Todo>> fetchTodos() async {
        final List<Map<String, dynamic>> maps = await _database!.query('todos');
        return maps.map((map) => Todo.fromMap(map)).toList();
      }
    
      Future<void> deleteTodo(int id) async {
        await _database!.delete(
          'todos',
          where: 'id = ?',
          whereArgs: [id],
        );
      }
    }
  5. Implement the API Client Service (Optional/If Applicable) Create a stateless service for remote data fetching.
    dart
    class ApiClient {
      Future<List<dynamic>> fetchRawTodos() async {
        // Implementation for HTTP GET request
        return []; 
      }
    }
  6. Implement the Repository Create the Repository class. This is the single source of truth for the application data. It must encapsulate the services as private members.
    dart
    class TodoRepository {
      final DatabaseService _databaseService;
      final ApiClient _apiClient;
    
      TodoRepository({
        required DatabaseService databaseService,
        required ApiClient apiClient,
      })  : _databaseService = databaseService,
            _apiClient = apiClient;
    
      Future<List<Todo>> getTodos() async {
        await _ensureDbOpen();
        // Example of offline-first logic: fetch local, optionally sync with remote
        return await _databaseService.fetchTodos();
      }
    
      Future<void> createTodo(Todo todo) async {
        await _ensureDbOpen();
        await _databaseService.insertTodo(todo);
        // Trigger API sync here if necessary
      }
    
      Future<void> removeTodo(int id) async {
        await _ensureDbOpen();
        await _databaseService.deleteTodo(id);
      }
    
      Future<void> _ensureDbOpen() async {
        if (!_databaseService.isOpen) {
          await _databaseService.open();
        }
      }
    }
  7. Validate-and-Fix Review the generated implementation against the following checks:
    • Check: Are the services (
      _databaseService
      ,
      _apiClient
      ) private members of the Repository? If not, refactor to restrict UI layer access.
    • Check: Does the Repository explicitly ensure the database is open before executing queries? If not, inject the
      _ensureDbOpen()
      pattern.
    • Check: Are primary keys (
      id
      ) used effectively in SQLite queries to optimize update/delete times?
  1. 分析数据需求 停下来询问用户: "数据层需要管理哪些具体的数据实体,它们的持久化要求是什么(例如大小、关系复杂度、离线优先能力)?" 进入步骤2前请等待用户回复。
  2. 配置依赖 根据决策逻辑添加所需依赖。如果是标准的SQLite实现,执行:
    bash
    flutter pub add sqflite path
  3. 定义领域模型 创建纯Dart数据类来表示领域模型。这些模型应该仅包含应用其他部分所需的信息。
    dart
    class Todo {
      final int? id;
      final String title;
      final bool isCompleted;
    
      const Todo({this.id, required this.title, required this.isCompleted});
    
      Map<String, dynamic> toMap() {
        return {
          'id': id,
          'title': title,
          'isCompleted': isCompleted ? 1 : 0,
        };
      }
    
      factory Todo.fromMap(Map<String, dynamic> map) {
        return Todo(
          id: map['id'] as int?,
          title: map['title'] as String,
          isCompleted: map['isCompleted'] == 1,
        );
      }
    }
  4. 实现数据库服务 创建无状态服务类来处理与SQLite数据库的直接交互。
    dart
    import 'package:path/path.dart';
    import 'package:sqflite/sqflite.dart';
    
    class DatabaseService {
      Database? _database;
    
      Future<void> open() async {
        if (_database != null && _database!.isOpen) return;
        
        _database = await openDatabase(
          join(await getDatabasesPath(), 'app_database.db'),
          onCreate: (db, version) {
            return db.execute(
              'CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, isCompleted INTEGER)',
            );
          },
          version: 1,
        );
      }
    
      bool get isOpen => _database != null && _database!.isOpen;
    
      Future<int> insertTodo(Todo todo) async {
        return await _database!.insert(
          'todos',
          todo.toMap(),
          conflictAlgorithm: ConflictAlgorithm.replace,
        );
      }
    
      Future<List<Todo>> fetchTodos() async {
        final List<Map<String, dynamic>> maps = await _database!.query('todos');
        return maps.map((map) => Todo.fromMap(map)).toList();
      }
    
      Future<void> deleteTodo(int id) async {
        await _database!.delete(
          'todos',
          where: 'id = ?',
          whereArgs: [id],
        );
      }
    }
  5. 实现API客户端服务(可选/按需) 创建用于远程数据拉取的无状态服务。
    dart
    class ApiClient {
      Future<List<dynamic>> fetchRawTodos() async {
        // Implementation for HTTP GET request
        return []; 
      }
    }
  6. 实现Repository 创建Repository类。这是应用数据的单一数据源。它必须将各服务封装为私有成员。
    dart
    class TodoRepository {
      final DatabaseService _databaseService;
      final ApiClient _apiClient;
    
      TodoRepository({
        required DatabaseService databaseService,
        required ApiClient apiClient,
      })  : _databaseService = databaseService,
            _apiClient = apiClient;
    
      Future<List<Todo>> getTodos() async {
        await _ensureDbOpen();
        // Example of offline-first logic: fetch local, optionally sync with remote
        return await _databaseService.fetchTodos();
      }
    
      Future<void> createTodo(Todo todo) async {
        await _ensureDbOpen();
        await _databaseService.insertTodo(todo);
        // Trigger API sync here if necessary
      }
    
      Future<void> removeTodo(int id) async {
        await _ensureDbOpen();
        await _databaseService.deleteTodo(id);
      }
    
      Future<void> _ensureDbOpen() async {
        if (!_databaseService.isOpen) {
          await _databaseService.open();
        }
      }
    }
  7. 验证与修复 对照以下检查项审查生成的实现:
    • 检查: 各服务(
      _databaseService
      _apiClient
      )是否是Repository的私有成员?如果不是,进行重构以限制UI层的访问。
    • 检查: Repository在执行查询前是否明确确保数据库已打开?如果不是,加入
      _ensureDbOpen()
      逻辑。
    • 检查: SQLite查询中是否有效使用主键(
      id
      )来优化更新/删除操作的耗时?

Constraints

约束条件

  • Single Source of Truth: The UI layer MUST NEVER interact directly with a Service (e.g.,
    DatabaseService
    or
    ApiClient
    ). All data requests must route through the Repository.
  • Stateless Services: Service classes must remain stateless and contain no side effects outside of their specific external API/DB wrapper responsibilities.
  • Domain Model Isolation: Repositories must transform raw data (from APIs or DBs) into Domain Models before passing them to the UI layer.
  • SQL Injection Prevention: Always use parameterized queries (e.g.,
    whereArgs: [id]
    ) in
    sqflite
    operations. Never use string interpolation for SQL queries.
  • Database State: The Repository must guarantee the database connection is open before attempting any read/write operations.
  • 单一数据源: UI层绝对不能直接与Service(例如
    DatabaseService
    ApiClient
    )交互。所有数据请求都必须经过Repository路由。
  • 无状态Service: Service类必须保持无状态,除了其特定的外部API/DB封装职责外,不得包含其他副作用。
  • 领域模型隔离: Repository必须将(来自API或DB的)原始数据转换为领域模型后再传递给UI层。
  • SQL注入防护:
    sqflite
    操作中始终使用参数化查询(例如
    whereArgs: [id]
    )。永远不要使用字符串插值拼接SQL查询。
  • 数据库状态: 在尝试任何读写操作前,Repository必须保证数据库连接已打开。