《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<bool> (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可以让代码更简洁。推荐 floor 或 moor:

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

5.3 优化技巧

  1. 使用索引:对经常查询的字段创建索引
  2. 批量操作 :使用 batch() 进行批量插入/更新
  3. 连接池:保持数据库连接,避免频繁开关
  4. 分页查询 :大数据集使用 LIMIT 和 OFFSET
  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开发的基础,掌握好它,就能开发出体验流畅、数据可靠的应用。希望这篇文章能帮助到你! 我们下期再见!

相关推荐
yuanlaile8 小时前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
Anhty13 小时前
2026最新免费手机音频处理工具 !!
android·功能测试·ios·智能手机·音视频
2501_9159090615 小时前
iOS应用从开发到上架App Store的完整发布流程与步骤指南
android·ios·小程序·https·uni-app·webview
初雪云16 小时前
UniApp 云打包拿到 IPA 后,真正的发布工作才刚开始
ios·自动化·编辑器·产品经理·iphone
Lancker18 小时前
没有 Mac 也能上架 iOS:GitHub Actions 全自动构建 + 签名 + TestFlight 送审实录
macos·ios·github
茶底世界之下18 小时前
晚霞要多暖?用 Awaking 只改色温做一次取舍
ios
恋猫de小郭19 小时前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白19 小时前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
酒神dnspup20 小时前
多地区网站可用性监控怎么做?DNSPup 从基线到告警的完整实战
运维·ios·云计算·iphone·dns·网络监控
福兮说1 天前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic