《Flutter全栈开发实战指南:从零到高级》- 15 -本地数据存储

Flutter本地存储

当我们关闭App再重新打开,为什么有些数据(比如登录状态、用户设置、文章草稿)还在,而有些数据(比如临时弹窗状态)却消失了?这背后就是 "本地存储" 在发挥作用。可以说,一个不懂得如何管理本地数据的开发者,很难做出用户体验好的应用。

今天,我们就来彻底搞懂Flutter中的本地存储。


一、 为什么需要本地存储?

举个例子:如果你每天醒来都会失忆,不记得自己的名字、家在哪里、昨天做了什么......这简直是一场灾难。对于App而言,本地存储就是它的 "记忆系统"

主要应用场景:

  1. 用户偏好设置:比如主题颜色、语言选择、消息提醒开关。
  2. 登录状态保持:用户登录后,App"记住"他,下次打开无需重新登录。
  3. 缓存网络数据:将首屏数据缓存下来,下次启动秒开,提升用户体验。
  4. 离线数据持久化:如笔记草稿、阅读进度、购物车商品,即使断网也不丢失。
  5. 大数据量结构化存储:比如聊天记录、交易明细等。

Flutter拥有多种本地数据存储方案,下面我们先看下用张图来了解下存储方案脉络:

二、 shared_preferences

2.1 它是什么?能干什么?

shared_preferences 这个名字听起来有点拗口,但其实很简单。你可以把它理解成 Flutter 为我们在本地提供的一个 "小本子",专门用来记录一些简单的、键值对形式的数据。

  • shared(共享):指这些数据在你的App内是共享的,任何页面都能读写。
  • preferences(偏好):顾名思义,最适合存储用户的偏好设置。

它的本质是什么? 在 Android 上,它背后是通过 SharedPreferences API 将数据以 XML 文件形式存储;在 iOS 上,则使用的是 NSUserDefaults。Flutter 插件帮我们统一了这两端的接口,让我们可以用一套代码搞定双端存储。

2.2 工作原理图解

让我们看看当你调用 setString('name', '一一') 时,背后发生了什么:

sequenceDiagram participant A as Flutter App participant SP as shared_preferences插件 participant M as Method Channel participant AOS as Android (SharedPreferences) participant IOS as iOS (NSUserDefaults) A->>SP: 调用 setString('name', '一一') SP->>M: 通过Method Channel调用原生代码 M->>AOS: (在Android上) 写入XML文件 M->>IOS: (在iOS上) 写入plist文件 AOS-->>M: 写入成功 IOS-->>M: 写入成功 M-->>SP: 返回结果 SP-->>A: 返回 Future (true)

关键点:

  • 异步操作 :所有读写操作都是 Future,意味着不会阻塞你的UI线程。
  • 持久化:数据被写入设备文件系统,App重启后依然存在。
  • 平台差异被屏蔽:你不需要关心底层是XML还是plist,插件帮你处理了。

2.3 下面用一段代码来详细介绍下

第一步:引入依赖pubspec.yaml 文件中添加:

yaml 复制代码
dependencies:
  shared_preferences: ^2.2.2 # 请使用最新版本

然后运行 flutter pub get

第二步:基础CRUD操作

dart 复制代码
import 'package:shared_preferences/shared_preferences.dart';

class SPManager {
  // 单例
  static final SPManager _instance = SPManager._internal();
  factory SPManager() => _instance;
  SPManager._internal();

  late SharedPreferences _prefs;

  // 初始化
  Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
    print('SharedPreferences 初始化完成!');
  }

  // 1. 写入数据
  Future<bool> saveUserInfo() async {
    try {
      // 字符串
      await _prefs.setString('username', 'Flutter本地存储');
      // 整型
      await _prefs.setInt('userAge', 28);
      // 布尔值
      await _prefs.setBool('isVip', true);
      // 字符串列表
      await _prefs.setStringList('hobbies', ['编程', '读书', '健身']);
      // 双精度浮点数
      await _prefs.setDouble('walletBalance', 99.99);

      print('用户信息保存成功!');
      return true;
    } catch (e) {
      print('保存失败: $e');
      return false;
    }
  }

  // 2. 读取数据
  void readUserInfo() {
    // 读取字符串,提供默认值
    String username = _prefs.getString('username') ?? '未知用户';
    int age = _prefs.getInt('userAge') ?? 0;
    bool isVip = _prefs.getBool('isVip') ?? false;
    double balance = _prefs.getDouble('walletBalance') ?? 0.0;
    List<String> hobbies = _prefs.getStringList('hobbies') ?? [];

    print('''
      用户信息:
      用户名:$username
      年龄:$age
      VIP:$isVip
      余额:$balance
      爱好:$hobbies
    ''');
  }

  // 3. 删除数据
  Future<bool> deleteUserInfo() async {
    try {
      // 删除指定键
      await _prefs.remove('username');
      // 清空所有数据
      // await _prefs.clear();
      print('用户信息已删除');
      return true;
    } catch (e) {
      print('删除失败: $e');
      return false;
    }
  }

  // 4. 检查键是否存在
  bool containsKey(String key) {
    return _prefs.containsKey(key);
  }

  // 5. 获取所有键
  Set<String> getAllKeys() {
    return _prefs.getKeys();
  }
}

第三步:在App中使用

dart 复制代码
void main() async {
  // 确保WidgetsBinding初始化
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化SPManager
  await SPManager().init();
  
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final SPManager _spManager = SPManager();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('SP演示')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => _spManager.saveUserInfo(),
              child: Text('保存用户信息'),
            ),
            ElevatedButton(
              onPressed: () => _spManager.readUserInfo(),
              child: Text('读取用户信息'),
            ),
            ElevatedButton(
              onPressed: () => _spManager.deleteUserInfo(),
              child: Text('删除用户信息'),
            ),
          ],
        ),
      ),
    );
  }
}

2.4 使用介绍与注意点

  1. 一定要先初始化 :在使用前必须调用 getInstance() 并等待其完成。

  2. 处理空值:读取时一定要提供默认值,因为键可能不存在。

  3. 不要存大数据:它不适合存储大型对象或列表,性能会变差。

  4. 键名管理 :建议使用常量来管理键名,避免拼写错误。

    dart 复制代码
    class SPKeys {
      static const String username = 'username';
      static const String userAge = 'user_age';
      static const String isVip = 'is_vip';
    }
  5. 异步错误处理 :使用 try-catch 包裹可能出错的操作。


三、 文件存储

3.1 适用场景

当你的数据不适合用键值对存储时,文件存储就派上用场了:

  • App的配置文件(JSON, XML)
  • 用户下载的图片、文档
  • 应用日志文件
  • 需要自定义格式的数据

3.2 文件系统路径详解

在Flutter中,我们使用 path_provider 插件来获取各种路径:

dart 复制代码
import 'package:path_provider/path_provider.dart';

class FilePathManager {
  // 获取临时目录
  static Future<String> get tempPath async {
    final dir = await getTemporaryDirectory();
    return dir.path;
  }

  // 获取文档目录(Android对应App专用目录,iOS对应Documents)
  static Future<String> get documentsPath async {
    final dir = await getApplicationDocumentsDirectory();
    return dir.path;
  }

  // 获取外部存储目录
  static Future<String?> get externalStoragePath async {
    final dir = await getExternalStorageDirectory();
    return dir?.path;
  }

  // 获取支持目录
  static Future<String> get supportPath async {
    final dir = await getApplicationSupportDirectory();
    return dir.path;
  }
}

路径选择:

  • 临时文件getTemporaryDirectory() - 缓存,系统可清理
  • 用户数据getApplicationDocumentsDirectory() - 用户生成的内容
  • App内部文件getApplicationSupportDirectory() - App运行所需文件

3.3 文件存储实战演示

一个完整的文件管理类如下代码所示:

dart 复制代码
import 'dart:io';
import 'dart:convert';
import 'package:path_provider/path_provider.dart';

class FileManager {
  // 单例
  static final FileManager _instance = FileManager._internal();
  factory FileManager() => _instance;
  FileManager._internal();

  // 获取文件路径
  Future<String> _getLocalFilePath(String filename) async {
    final dir = await getApplicationDocumentsDirectory();
    return '${dir.path}/$filename';
  }

  // 1. 写入字符串到文件
  Future<File> writeStringToFile(String content, String filename) async {
    try {
      final file = File(await _getLocalFilePath(filename));
      return await file.writeAsString(content);
    } catch (e) {
      print('写入文件失败: $e');
      rethrow;
    }
  }

  // 2. 从文件读取字符串
  Future<String> readStringFromFile(String filename) async {
    try {
      final file = File(await _getLocalFilePath(filename));
      if (await file.exists()) {
        return await file.readAsString();
      } else {
        throw Exception('文件不存在');
      }
    } catch (e) {
      print('读取文件失败: $e');
      rethrow;
    }
  }

  // 3. 写入JSON对象
  Future<File> writeJsonToFile(Map<String, dynamic> json, String filename) async {
    final jsonString = jsonEncode(json);
    return await writeStringToFile(jsonString, filename);
  }

  // 4. 从文件读取JSON对象
  Future<Map<String, dynamic>> readJsonFromFile(String filename) async {
    try {
      final jsonString = await readStringFromFile(filename);
      return jsonDecode(jsonString);
    } catch (e) {
      print('读取JSON失败: $e');
      rethrow;
    }
  }

  // 5. 增加内容到文件
  Future<File> appendToFile(String content, String filename) async {
    try {
      final file = File(await _getLocalFilePath(filename));
      return await file.writeAsString(content, mode: FileMode.append);
    } catch (e) {
      print('追加文件失败: $e');
      rethrow;
    }
  }

  // 6. 检查文件是否存在
  Future<bool> fileExists(String filename) async {
    final file = File(await _getLocalFilePath(filename));
    return await file.exists();
  }

  // 7. 删除文件
  Future<void> deleteFile(String filename) async {
    try {
      final file = File(await _getLocalFilePath(filename));
      if (await file.exists()) {
        await file.delete();
        print('文件删除成功: $filename');
      }
    } catch (e) {
      print('删除文件失败: $e');
      rethrow;
    }
  }

  // 8. 获取文件信息
  Future<FileStat> getFileInfo(String filename) async {
    try {
      final file = File(await _getLocalFilePath(filename));
      if (await file.exists()) {
        return await file.stat();
      } else {
        throw Exception('文件不存在');
      }
    } catch (e) {
      print('获取文件信息失败: $e');
      rethrow;
    }
  }
}

3.4 以用户配置管理为例:

dart 复制代码
class UserConfigManager {
  static const String _configFileName = 'user_config.json';
  final FileManager _fileManager = FileManager();

  // 保存用户配置
  Future<void> saveUserConfig({
    required String theme,
    required String language,
    required bool darkMode,
    required List<String> recentSearches,
  }) async {
    final config = {
      'theme': theme,
      'language': language,
      'darkMode': darkMode,
      'recentSearches': recentSearches,
      'lastUpdated': DateTime.now().toIso8601String(),
    };

    await _fileManager.writeJsonToFile(config, _configFileName);
    print('用户配置已保存');
  }

  // 读取用户配置
  Future<Map<String, dynamic>> loadUserConfig() async {
    try {
      if (await _fileManager.fileExists(_configFileName)) {
        return await _fileManager.readJsonFromFile(_configFileName);
      } else {
        // 返回默认配置
        return _getDefaultConfig();
      }
    } catch (e) {
      print('加载用户配置失败,使用默认配置: $e');
      return _getDefaultConfig();
    }
  }

  Map<String, dynamic> _getDefaultConfig() {
    return {
      'theme': 'light',
      'language': 'zh-CN',
      'darkMode': false,
      'recentSearches': [],
      'lastUpdated': DateTime.now().toIso8601String(),
    };
  }

  // 清空配置
  Future<void> clearConfig() async {
    await _fileManager.deleteFile(_configFileName);
  }
}

四、 SQLite

4.1 什么是SQLite?为什么需要它?

SQLite是一个轻量级的、文件式的关系型数据库。它不需要单独的服务器进程,整个数据库就是一个文件,非常适合移动端应用。

使用场景:

  • 用户关系管理(联系人、好友)
  • 商品目录、订单管理
  • 聊天消息记录
  • 任何需要复杂查询和关系的数据

4.2 Flutter中的SQLite架构

在Flutter中,我们通常使用 sqflite 插件来操作SQLite:

graph TB A[Flutter App] --> B[sqflite插件] B --> C[Method Channel] C --> D[Android: SQLiteDatabase] C --> E[iOS: SQLite3 Library] D --> F[.db文件] E --> F F --> G[数据持久化]

4.3 构建一个任务管理App

第一步:添加依赖

yaml 复制代码
dependencies:
  sqflite: ^2.3.0
  path: ^1.8.3

第二步:创建数据库工具类

dart 复制代码
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  factory DatabaseHelper() => _instance;
  DatabaseHelper._internal();

  static Database? _database;

  // 数据库名称和版本
  static const String _dbName = 'task_manager.db';
  static const int _dbVersion = 1;

  // 表名和列名
  static const String tableTasks = 'tasks';
  static const String columnId = 'id';
  static const String columnTitle = 'title';
  static const String columnDescription = 'description';
  static const String columnIsCompleted = 'is_completed';
  static const String columnCreatedAt = 'created_at';
  static const String columnUpdatedAt = 'updated_at';

  // 获取数据库实例
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  // 初始化数据库
  Future<Database> _initDatabase() async {
    // 获取数据库路径
    String path = join(await getDatabasesPath(), _dbName);
    
    // 创建/打开数据库
    return await openDatabase(
      path,
      version: _dbVersion,
      onCreate: _createTables,
      onUpgrade: _upgradeDatabase,
    );
  }

  // 创建表
  Future<void> _createTables(Database db, int version) async {
    await db.execute('''
      CREATE TABLE $tableTasks (
        $columnId INTEGER PRIMARY KEY AUTOINCREMENT,
        $columnTitle TEXT NOT NULL,
        $columnDescription TEXT,
        $columnIsCompleted INTEGER NOT NULL DEFAULT 0,
        $columnCreatedAt INTEGER NOT NULL,
        $columnUpdatedAt INTEGER NOT NULL
      )
    ''');
    print('任务表创建成功!');
  }

  // 数据库升级
  Future<void> _upgradeDatabase(Database db, int oldVersion, int newVersion) async {
    if (oldVersion < 2) {
      // await db.execute('ALTER TABLE $tableTasks ADD COLUMN new_column TEXT');
    }
    print('数据库从版本 $oldVersion 升级到 $newVersion');
  }

  // 关闭数据库
  Future<void> close() async {
    if (_database != null) {
      await _database!.close();
      _database = null;
    }
  }
}

第三步:创建数据模型

dart 复制代码
class Task {
  int? id;
  String title;
  String? description;
  bool isCompleted;
  DateTime createdAt;
  DateTime updatedAt;

  Task({
    this.id,
    required this.title,
    this.description,
    this.isCompleted = false,
    DateTime? createdAt,
    DateTime? updatedAt,
  })  : createdAt = createdAt ?? DateTime.now(),
        updatedAt = updatedAt ?? DateTime.now();

  // 将Task对象转换为Map,便于存入数据库
  Map<String, dynamic> toMap() {
    return {
      DatabaseHelper.columnId: id,
      DatabaseHelper.columnTitle: title,
      DatabaseHelper.columnDescription: description,
      DatabaseHelper.columnIsCompleted: isCompleted ? 1 : 0,
      DatabaseHelper.columnCreatedAt: createdAt.millisecondsSinceEpoch,
      DatabaseHelper.columnUpdatedAt: updatedAt.millisecondsSinceEpoch,
    };
  }

  // 从Map创建Task对象
  factory Task.fromMap(Map<String, dynamic> map) {
    return Task(
      id: map[DatabaseHelper.columnId],
      title: map[DatabaseHelper.columnTitle],
      description: map[DatabaseHelper.columnDescription],
      isCompleted: map[DatabaseHelper.columnIsCompleted] == 1,
      createdAt: DateTime.fromMillisecondsSinceEpoch(
          map[DatabaseHelper.columnCreatedAt]),
      updatedAt: DateTime.fromMillisecondsSinceEpoch(
          map[DatabaseHelper.columnUpdatedAt]),
    );
  }

  @override
  String toString() {
    return 'Task{id: $id, title: $title, completed: $isCompleted}';
  }
}

第四步:创建数据访问对象

dart 复制代码
class TaskDao {
  final DatabaseHelper _dbHelper = DatabaseHelper();

  // 1. 插入新任务
  Future<int> insertTask(Task task) async {
    final db = await _dbHelper.database;
    
    // 更新时间戳
    task.updatedAt = DateTime.now();
    
    final id = await db.insert(
      DatabaseHelper.tableTasks,
      task.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
    
    print('任务创建成功,ID: $id');
    return id;
  }

  // 2. 根据ID查询任务
  Future<Task?> getTaskById(int id) async {
    final db = await _dbHelper.database;
    
    final maps = await db.query(
      DatabaseHelper.tableTasks,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id],
    );
    
    if (maps.isNotEmpty) {
      return Task.fromMap(maps.first);
    }
    return null;
  }

  // 3. 查询所有任务
  Future<List<Task>> getAllTasks() async {
    final db = await _dbHelper.database;
    
    final maps = await db.query(
      DatabaseHelper.tableTasks,
      orderBy: '${DatabaseHelper.columnCreatedAt} DESC',
    );
    
    return maps.map((map) => Task.fromMap(map)).toList();
  }

  // 4. 查询未完成的任务
  Future<List<Task>> getIncompleteTasks() async {
    final db = await _dbHelper.database;
    
    final maps = await db.query(
      DatabaseHelper.tableTasks,
      where: '${DatabaseHelper.columnIsCompleted} = ?',
      whereArgs: [0],
      orderBy: '${DatabaseHelper.columnCreatedAt} DESC',
    );
    
    return maps.map((map) => Task.fromMap(map)).toList();
  }

  // 5. 更新任务
  Future<int> updateTask(Task task) async {
    final db = await _dbHelper.database;
    
    // 更新修改时间
    task.updatedAt = DateTime.now();
    
    final count = await db.update(
      DatabaseHelper.tableTasks,
      task.toMap(),
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [task.id],
    );
    
    if (count > 0) {
      print('任务更新成功: ${task.title}');
    }
    
    return count;
  }

  // 6. 删除任务
  Future<int> deleteTask(int id) async {
    final db = await _dbHelper.database;
    
    final count = await db.delete(
      DatabaseHelper.tableTasks,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id],
    );
    
    if (count > 0) {
      print('任务删除成功, ID: $id');
    }
    
    return count;
  }

  // 7. 批量操作
  Future<void> batchInsertTasks(List<Task> tasks) async {
    final db = await _dbHelper.database;
    
    final batch = db.batch();
    
    for (final task in tasks) {
      batch.insert(DatabaseHelper.tableTasks, task.toMap());
    }
    
    await batch.commit();
    print('批量插入 ${tasks.length} 个任务成功');
  }

  // 8. 复杂查询:搜索任务
  Future<List<Task>> searchTasks(String keyword) async {
    final db = await _dbHelper.database;
    
    final maps = await db.query(
      DatabaseHelper.tableTasks,
      where: '''
        ${DatabaseHelper.columnTitle} LIKE ? OR 
        ${DatabaseHelper.columnDescription} LIKE ?
      ''',
      whereArgs: ['%$keyword%', '%$keyword%'],
      orderBy: '${DatabaseHelper.columnCreatedAt} DESC',
    );
    
    return maps.map((map) => Task.fromMap(map)).toList();
  }

  // 9. 事务操作
  Future<void> markAllAsCompleted() async {
    final db = await _dbHelper.database;
    
    await db.transaction((txn) async {
      await txn.update(
        DatabaseHelper.tableTasks,
        {
          DatabaseHelper.columnIsCompleted: 1,
          DatabaseHelper.columnUpdatedAt: DateTime.now().millisecondsSinceEpoch,
        },
      );
    });
    
    print('所有任务标记为完成');
  }
}

第五步:在UI中使用

dart 复制代码
class TaskListPage extends StatefulWidget {
  @override
  _TaskListPageState createState() => _TaskListPageState();
}

class _TaskListPageState extends State<TaskListPage> {
  final TaskDao _taskDao = TaskDao();
  List<Task> _tasks = [];
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadTasks();
  }

  Future<void> _loadTasks() async {
    setState(() => _isLoading = true);
    
    try {
      final tasks = await _taskDao.getAllTasks();
      setState(() => _tasks = tasks);
    } catch (e) {
      print('加载任务失败: $e');
      // 错误提示
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _addTask() async {
    final newTask = Task(
      title: '新任务 ${DateTime.now().second}',
      description: '这是一个新任务的描述',
    );
    
    await _taskDao.insertTask(newTask);
    await _loadTasks(); // 重新加载列表
  }

  Future<void> _toggleTaskCompletion(Task task) async {
    task.isCompleted = !task.isCompleted;
    await _taskDao.updateTask(task);
    await _loadTasks();
  }

  Future<void> _deleteTask(Task task) async {
    if (task.id != null) {
      await _taskDao.deleteTask(task.id!);
      await _loadTasks();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('任务管理器 (${_tasks.length})'),
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: _addTask,
          ),
        ],
      ),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : _tasks.isEmpty
              ? Center(child: Text('还没有任务,点击+号添加吧!'))
              : ListView.builder(
                  itemCount: _tasks.length,
                  itemBuilder: (context, index) {
                    final task = _tasks[index];
                    return Dismissible(
                      key: Key(task.id.toString()),
                      background: Container(color: Colors.red),
                      onDismissed: (_) => _deleteTask(task),
                      child: ListTile(
                        leading: Checkbox(
                          value: task.isCompleted,
                          onChanged: (_) => _toggleTaskCompletion(task),
                        ),
                        title: Text(
                          task.title,
                          style: TextStyle(
                            decoration: task.isCompleted
                                ? TextDecoration.lineThrough
                                : TextDecoration.none,
                          ),
                        ),
                        subtitle: Text(
                          task.description ?? '暂无描述',
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                        trailing: Text(
                          DateFormat('MM-dd HH:mm').format(task.createdAt),
                          style: TextStyle(fontSize: 12, color: Colors.grey),
                        ),
                      ),
                    );
                  },
                ),
    );
  }
}

五、 性能优化

5.1 数据库迁移

当你的数据结构需要变更时,就需要数据库迁移:

dart 复制代码
class DatabaseHelper {
  static const int _dbVersion = 2; // 版本升级
  
  Future<void> _upgradeDatabase(Database db, int oldVersion, int newVersion) async {
    for (int version = oldVersion + 1; version <= newVersion; version++) {
      switch (version) {
        case 2:
          await _migrateToV2(db);
          break;
        case 3:
          await _migrateToV3(db);
          break;
      }
    }
  }

  Future<void> _migrateToV2(Database db) async {
    // 添加优先级字段
    await db.execute('''
      ALTER TABLE ${DatabaseHelper.tableTasks} 
      ADD COLUMN priority INTEGER NOT NULL DEFAULT 0
    ''');
    print('数据库迁移到版本2成功');
  }

  Future<void> _migrateToV3(Database db) async {
    // 创建新表或更复杂的迁移
    await db.execute('''
      CREATE TABLE categories (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        color TEXT NOT NULL
      )
    ''');
    print('数据库迁移到版本3成功');
  }
}

5.2 使用ORM简化操作

虽然直接使用SQL很强大,但ORM可以让代码更简洁。推荐 floormoor

yaml 复制代码
dependencies:
  floor: ^1.4.0
  sqflite: ^2.0.0

5.3 优化技巧

  1. 使用索引:对经常查询的字段创建索引
  2. 批量操作 :使用 batch() 进行批量插入/更新
  3. 连接池:保持数据库连接,避免频繁开关
  4. 分页查询 :大数据集使用 LIMITOFFSET
  5. 避免N+1查询 :使用 JOIN 一次性获取关联数据
dart 复制代码
// 分页查询
Future<List<Task>> getTasksPaginated(int page, int pageSize) async {
  final db = await _dbHelper.database;
  final offset = (page - 1) * pageSize;
  
  final maps = await db.query(
    DatabaseHelper.tableTasks,
    limit: pageSize,
    offset: offset,
    orderBy: '${DatabaseHelper.columnCreatedAt} DESC',
  );
  
  return maps.map((map) => Task.fromMap(map)).toList();
}

六、 方案对比

下面我们通过多维度对以上几种本地存储方案进行一个详细对比:

维度 shared_preferences 文件存储 SQLite Hive
数据类型 基本类型 任意数据 结构化数据 任意对象
查询能力 键值查询 顺序读取 复杂SQL查询 键值+条件查询
性能 中等 快(有索引) 非常快
复杂度 简单 中等 复杂 简单
数据量 小(<1MB) 中等
是否需要序列化 需要 需要 需要 不需要

实际项目开发中,我们如何选择本地存储?可按下面策略进行存储方案选型:

graph TD A[开始选型] --> B{数据量大小}; B -->|很小 < 1MB| C{数据类型}; B -->|中等| D[文件存储]; B -->|很大| E{是否需要复杂查询}; C -->|简单键值对| F[shared_preferences]; C -->|复杂对象| G[Hive]; E -->|是| H[SQLite]; E -->|否| G; F --> I[完成]; D --> I; G --> I; H --> I;

以上选型策略概述以下:

  1. 用户设置、登录令牌shared_preferences
  2. App配置、日志文件 → 文件存储
  3. 聊天记录、商品目录 → SQLite
  4. 缓存数据、临时状态 → Hive
  5. 需要极致性能 → Hive
  6. 需要复杂关系查询 → SQLite

七、 综合应用代码实战

下面我们构建一个完整的用户数据管理方案,综合运用多种存储方式:

dart 复制代码
class UserDataManager {
  final SPManager _spManager = SPManager();
  final FileManager _fileManager = FileManager();
  final TaskDao _taskDao = TaskDao();
  
  // 1. 用户登录状态 - 使用shared_preferences
  Future<void> saveLoginState(User user) async {
    await _spManager.init();
    await _spManager._prefs.setString('user_id', user.id);
    await _spManager._prefs.setString('user_token', user.token);
    await _spManager._prefs.setBool('is_logged_in', true);
    
    // 同时保存用户信息到SQLite
    // await _userDao.insertUser(user);
  }
  
  // 2. 用户偏好设置 - 使用文件存储
  Future<void> saveUserPreferences(UserPreferences prefs) async {
    await _fileManager.writeJsonToFile(
      prefs.toJson(), 
      'user_preferences.json'
    );
  }
  
  // 3. 用户任务数据 - 使用SQLite
  Future<void> syncUserTasks(List<Task> tasks) async {
    await _taskDao.batchInsertTasks(tasks);
  }
  
  // 4. 清理所有用户数据
  Future<void> clearAllUserData() async {
    // 清理SP
    await _spManager._prefs.clear();
    
    // 清理配置文件
    await _fileManager.deleteFile('user_preferences.json');
    
    // 清理数据库
    // await _taskDao.deleteAllTasks();
  }
}

写在最后

至此本地数据存储的知识就全学完了,记住这几个核心要点:

  1. 性能意识:大数据量时考虑索引、分页、批量操作
  2. 错误处理:存储操作可能失败,一定要有完善的错误处理
  3. 数据安全:敏感信息考虑加密存储
  4. 测试验证:数据库迁移等复杂操作要充分测试

本地存储是App开发的基础,掌握好它,就能开发出体验流畅、数据可靠的应用。希望这篇文章能帮助到你! 我们下期再见!

相关推荐
非专业程序员4 小时前
精读GitHub - swift-markdown-ui
ios·swiftui·swift
法的空间4 小时前
让 Flutter 资源管理更智能
android·flutter·ios
江上清风山间明月5 小时前
Flutter中Column中使用ListView时溢出问题的解决方法
android·flutter·column·listview
恋猫de小郭6 小时前
Snapchat 开源全新跨平台框架 Valdi ,一起来搞懂它究竟有什么特别之处
android·前端·flutter
西西学代码9 小时前
Flutter---函数
flutter
西西学代码9 小时前
Flutter---Stream
java·服务器·flutter
GISer_Jing14 小时前
2025年Flutter与React Native对比
android·flutter·react native
2501_9159184116 小时前
移动端 HTTPS 抓包实战,多工具组合分析与高效排查指南
数据库·网络协议·ios·小程序·https·uni-app·iphone
Digitally16 小时前
解决“Move to iOS 卡在准备中”的 9 种有效方法
macos·ios·cocoa