You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter本地SQL数据库(sqflite)与Firestore在线同步实现求助

Flutter 本地sqflite与Firestore双向同步实现方案

以下是完整的实现思路和代码示例,涵盖本地数据库操作、Firestore交互以及联网自动同步逻辑:

1. 依赖配置

在pubspec.yaml中添加所需依赖:

dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.3.2
  path_provider: ^2.1.1
  cloud_firestore: ^4.15.5
  connectivity_plus: ^5.0.1

2. 数据模型定义

定义统一的数据模型,同时适配本地SQL和Firestore:

class Todo {
  final String id;
  final String title;
  final String content;
  final bool isSynced; // 标记是否已同步到Firestore
  final DateTime updatedAt;

  Todo({
    required this.id,
    required this.title,
    required this.content,
    this.isSynced = false,
    required this.updatedAt,
  });

  // 从SQL查询结果转模型
  factory Todo.fromSql(Map<String, dynamic> map) {
    return Todo(
      id: map['id'],
      title: map['title'],
      content: map['content'],
      isSynced: map['is_synced'] == 1,
      updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updated_at']),
    );
  }

  // 模型转SQL插入/更新数据
  Map<String, dynamic> toSql() {
    return {
      'id': id,
      'title': title,
      'content': content,
      'is_synced': isSynced ? 1 : 0,
      'updated_at': updatedAt.millisecondsSinceEpoch,
    };
  }

  // 从Firestore文档转模型
  factory Todo.fromFirestore(Map<String, dynamic> data, String documentId) {
    return Todo(
      id: documentId,
      title: data['title'],
      content: data['content'],
      isSynced: true,
      updatedAt: (data['updated_at'] as Timestamp).toDate(),
    );
  }

  // 模型转Firestore数据
  Map<String, dynamic> toFirestore() {
    return {
      'title': title,
      'content': content,
      'updated_at': Timestamp.fromDate(updatedAt),
    };
  }
}

3. 本地SQL数据库操作

创建LocalDatabase类封装sqflite操作:

import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';

class LocalDatabase {
  static Database? _database;
  static const String tableName = 'todos';

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDb();
    return _database!;
  }

  // 初始化数据库
  Future<Database> _initDb() async {
    final documentsDir = await getApplicationDocumentsDirectory();
    final path = join(documentsDir.path, 'todo_database.db');
    return await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE $tableName (
            id TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            content TEXT,
            is_synced INTEGER NOT NULL DEFAULT 0,
            updated_at INTEGER NOT NULL
          )
        ''');
      },
    );
  }

  // 插入或更新本地数据
  Future<void> upsertTodo(Todo todo) async {
    final db = await database;
    await db.insert(
      tableName,
      todo.toSql(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  // 获取所有未同步的本地数据
  Future<List<Todo>> getUnsyncedTodos() async {
    final db = await database;
    final maps = await db.query(
      tableName,
      where: 'is_synced = ?',
      whereArgs: [0],
    );
    return maps.map((map) => Todo.fromSql(map)).toList();
  }

  // 获取所有本地数据
  Future<List<Todo>> getAllTodos() async {
    final db = await database;
    final maps = await db.query(tableName);
    return maps.map((map) => Todo.fromSql(map)).toList();
  }

  // 标记数据为已同步
  Future<void> markAsSynced(String todoId) async {
    final db = await database;
    await db.update(
      tableName,
      {'is_synced': 1},
      where: 'id = ?',
      whereArgs: [todoId],
    );
  }

  // 根据ID删除本地数据
  Future<void> deleteTodo(String id) async {
    final db = await database;
    await db.delete(
      tableName,
      where: 'id = ?',
      whereArgs: [id],
    );
  }
}

4. Firestore操作封装

创建FirestoreService类处理云端数据交互:

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
  final CollectionReference _todosCollection = FirebaseFirestore.instance.collection('todos');

  // 上传本地数据到Firestore
  Future<void> uploadTodo(Todo todo) async {
    await _todosCollection.doc(todo.id).set(todo.toFirestore());
  }

  // 从Firestore获取所有数据
  Future<List<Todo>> getAllRemoteTodos() async {
    final snapshot = await _todosCollection.get();
    return snapshot.docs.map((doc) => Todo.fromFirestore(doc.data() as Map<String, dynamic>, doc.id)).toList();
  }

  // 更新Firestore数据
  Future<void> updateTodo(Todo todo) async {
    await _todosCollection.doc(todo.id).update(todo.toFirestore());
  }

  // 删除Firestore数据
  Future<void> deleteTodo(String id) async {
    await _todosCollection.doc(id).delete();
  }
}

5. 同步逻辑实现

创建SyncService类处理双向同步,结合网络监听:

import 'package:connectivity_plus/connectivity_plus.dart';

class SyncService {
  final LocalDatabase localDb;
  final FirestoreService firestoreService;
  final Connectivity connectivity = Connectivity();

  SyncService({required this.localDb, required this.firestoreService}) {
    // 监听网络状态变化,联网时触发同步
    connectivity.onConnectivityChanged.listen((result) {
      if (result != ConnectivityResult.none) {
        syncData();
      }
    });
  }

  // 双向同步主方法
  Future<void> syncData() async {
    // 1. 先将本地未同步数据上传到Firestore
    await syncLocalToRemote();
    // 2. 再将Firestore最新数据同步到本地
    await syncRemoteToLocal();
  }

  // 本地→云端同步
  Future<void> syncLocalToRemote() async {
    final unsyncedTodos = await localDb.getUnsyncedTodos();
    for (var todo in unsyncedTodos) {
      await firestoreService.uploadTodo(todo);
      await localDb.markAsSynced(todo.id);
    }
  }

  // 云端→本地同步(处理冲突:保留更新时间更近的数据)
  Future<void> syncRemoteToLocal() async {
    final remoteTodos = await firestoreService.getAllRemoteTodos();
    final localTodos = await localDb.getAllTodos();

    // 构建本地数据ID映射
    final localTodoMap = {for (var t in localTodos) t.id: t};

    for (var remoteTodo in remoteTodos) {
      final localTodo = localTodoMap[remoteTodo.id];
      if (localTodo == null) {
        // 本地不存在,插入
        await localDb.upsertTodo(remoteTodo);
      } else {
        // 存在则比较更新时间,保留最新的
        if (remoteTodo.updatedAt.isAfter(localTodo.updatedAt)) {
          await localDb.upsertTodo(remoteTodo);
        } else if (!localTodo.isSynced) {
          // 本地更新更近且未同步,重新上传
          await firestoreService.updateTodo(localTodo);
          await localDb.markAsSynced(localTodo.id);
        }
      }
    }

    // 删除本地存在但云端已删除的数据
    final remoteIds = remoteTodos.map((t) => t.id).toSet();
    for (var localTodo in localTodos) {
      if (!remoteIds.contains(localTodo.id)) {
        await localDb.deleteTodo(localTodo.id);
      }
    }
  }
}

6. 页面中使用示例

在主页面初始化服务并测试:

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Local-Firestore Sync',
      home: const TodoPage(),
    );
  }
}

class TodoPage extends StatefulWidget {
  const TodoPage({super.key});

  @override
  State<TodoPage> createState() => _TodoPageState();
}

class _TodoPageState extends State<TodoPage> {
  late final LocalDatabase _localDb;
  late final FirestoreService _firestoreService;
  late final SyncService _syncService;
  List<Todo> _todos = [];
  final _titleController = TextEditingController();
  final _contentController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _localDb = LocalDatabase();
    _firestoreService = FirestoreService();
    _syncService = SyncService(localDb: _localDb, firestoreService: _firestoreService);
    _loadLocalTodos();
    // 初始检查网络,若联网则同步
    _checkNetworkAndSync();
  }

  Future<void> _loadLocalTodos() async {
    final todos = await _localDb.getAllTodos();
    setState(() => _todos = todos);
  }

  Future<void> _checkNetworkAndSync() async {
    final result = await Connectivity().checkConnectivity();
    if (result != ConnectivityResult.none) {
      await _syncService.syncData();
      _loadLocalTodos();
    }
  }

  Future<void> _addTodo() async {
    final todo = Todo(
      id: const Uuid().v4(),
      title: _titleController.text,
      content: _contentController.text,
      isSynced: false,
      updatedAt: DateTime.now(),
    );
    await _localDb.upsertTodo(todo);
    setState(() => _todos.add(todo));
    _titleController.clear();
    _contentController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('待办列表')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                TextField(
                  controller: _titleController,
                  decoration: const InputDecoration(labelText: '标题'),
                ),
                TextField(
                  controller: _contentController,
                  decoration: const InputDecoration(labelText: '内容'),
                ),
                ElevatedButton(onPressed: _addTodo, child: const Text('添加待办')),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) {
                final todo = _todos[index];
                return ListTile(
                  title: Text(todo.title),
                  subtitle: Text(todo.content),
                  trailing: todo.isSynced
                      ? const Icon(Icons.cloud_done, color: Colors.green)
                      : const Icon(Icons.cloud_upload_outlined, color: Colors.grey),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

关键注意事项

  • 冲突处理:示例中通过updatedAt时间戳判断数据版本,实际项目可根据需求调整冲突策略(如用户确认、字段合并等)
  • 网络监听:使用connectivity_plus实时监听网络状态,确保联网时自动触发同步
  • 数据唯一性:使用UUID确保本地和云端数据ID一致,避免重复
  • 错误处理:示例省略了try-catch,实际开发中需添加异常捕获,处理网络请求失败、数据库操作失败等情况

内容的提问来源于stack exchange,提问作者iamshyley

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 05:25:01