第十五讲 本地存储

0 阅读11分钟

前言:

这一章学完,通用的90%功能都能开发了,也就是说,已经可以开始vibecoding写代码了,在这个过程中,除了模型的代码外,你一定要学会排查问题,这是你学习这些知识点的目的。通过熟悉业务、熟悉需求,去控制生成代码,在深入细节处,生成代码或许就不足了,这个时候,你需要在细微处指导他怎么操作,就跟教导婴儿一样,你会发现,这十五讲的内容走下去,你已经具备了发现问题的内容。

至于深入能力,在你不断解决问题中就积累了。

一、总览

本讲聚焦 Flutter 应用中本地数据持久化的核心技术,解决“应用重启后数据不丢失”的问题。在移动开发中,本地存储是实现用户偏好设置、离线数据缓存、本地数据库等功能的基础。

  1. 存储方案选型

    1. 简单键值对(设置、状态)→ SharedPreferences;
    2. 中等规模结构化数据(收藏、缓存)→ Hive;
    3. 复杂关系型数据(订单、用户)→ Drift/SQLite;
    4. 大文件(图片、日志)→ 文件读写。
  2. 数据安全原则

    1. 敏感数据(Token、密码)必须加密存储;
    2. 密码使用不可逆哈希(SHA-256+盐),不存储明文;
    3. 密钥不要硬编码,建议动态获取。
  3. 性能与最佳实践

    1. Hive性能优于SharedPreferences,适合高频读写;
    2. Drift/SQLite适合复杂查询和关系型数据;
    3. 文件操作需处理异常,大文件分块读写。
  • 本讲解决的核心问题:如何在 Flutter 应用中安全、高效地将数据保存到设备本地,以及如何读取、更新、删除这些数据
  • 技术选型逻辑:根据数据类型(键值对/结构化数据/文件)、性能要求、复杂度选择对应的存储方案;
  • 应用场景:用户登录状态保存、APP 主题/语言设置、离线商品列表、本地日志文件、加密存储敏感信息(如token)等。

Flutter 本地存储的底层是对原生平台(Android/iOS)存储能力的封装,核心原理结构如下:

image.png

  1. 跨平台封装:Flutter 本地存储方案本质是对 Android/iOS 原生存储API的封装,保证跨平台一致性;

  2. 存储介质:所有本地存储最终都落地到设备的文件系统(沙盒目录),不同方案只是数据组织和读写方式不同;

  3. 分层设计:应用层通过抽象层调用存储API,加密层可对敏感数据进行加密后再存储;

  4. 数据形态

    1. 键值对存储(SharedPreferences/Hive):适合简单数据;
    2. 结构化存储(SQLite/Drift):适合复杂关系型数据;
    3. 文件存储:适合大文件(图片、日志、二进制数据)。

二、核心知识点

2.1 轻量存储:SharedPreferences

这个在上一讲的cache管理中有简单的使用到,这里详细的讲解一下。

核心概念

SharedPreferences 是 Flutter 中最基础的键值对存储方案,基于原生平台的轻量存储(Android SharedPreferences / iOS NSUserDefaults),适合存储简单数据(字符串、数字、布尔值等)。

前置依赖
flutter pub add shared_preferences
核心方法/属性
方法作用支持的数据类型
SharedPreferences.getInstance()获取实例(异步)-
setString/setInt/setBool/setDouble/setStringList保存数据String/int/bool/double/List
getString/getInt/getBool/getDouble/getStringList读取数据返回对应类型或null
remove(key)删除指定键的数据-
clear()清空所有数据-
containsKey(key)判断是否包含指定键返回bool
案例代码
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SharedPreferences 示例',
      home: const SharedPrefsDemo(),
    );
  }
}

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

  @override
  State<SharedPrefsDemo> createState() => _SharedPrefsDemoState();
}

class _SharedPrefsDemoState extends State<SharedPrefsDemo> {
  String _username = '';
  bool _isDarkMode = false;
  int _loginCount = 0;

  // 初始化:读取存储的数据
  @override
  void initState() {
    super.initState();
    _loadData();
  }

  // 读取数据
  Future<void> _loadData() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _username = prefs.getString('username') ?? '';
      _isDarkMode = prefs.getBool('dark_mode') ?? false;
      _loginCount = prefs.getInt('login_count') ?? 0;
    });
  }

  // 保存数据
  Future<void> _saveData() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('username', 'FlutterDev');
    await prefs.setBool('dark_mode', _isDarkMode);
    await prefs.setInt('login_count', _loginCount + 1);
    // 重新加载数据
    _loadData();
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('数据保存成功!')),
      );
    }
  }

  // 清空数据
  Future<void> _clearData() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
    _loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('SharedPreferences 演示')),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('用户名:$_username'),
            Text('暗黑模式:${_isDarkMode ? '开启' : '关闭'}'),
            Text('登录次数:$_loginCount'),
            const SizedBox(height: 20),
            SwitchListTile(
              title: const Text('开启暗黑模式'),
              value: _isDarkMode,
              onChanged: (value) => setState(() => _isDarkMode = value),
            ),
            const SizedBox(height: 20),
            Row(
              children: [
                ElevatedButton(
                  onPressed: _saveData,
                  child: const Text('保存数据'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _clearData,
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                  child: const Text('清空数据'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

注意事项
  1. 异步操作:所有读写操作依赖 SharedPreferences.getInstance() 的异步结果,需用 async/await
  2. 数据限制:仅支持基础数据类型,不支持自定义对象(需手动序列化/反序列化);
  3. 性能问题:不适合存储大量数据,频繁读写会影响性能;
  4. 持久化时机set 方法调用后数据会异步写入磁盘,并非立即持久化。

2.2 NoSQL 存储:Hive

核心概念

Hive 是 Flutter 专属的轻量级 NoSQL 键值数据库,无需原生依赖,支持自定义对象、索引、加密,性能优于 SharedPreferences,适合存储中等规模的结构化数据。

前置依赖
flutter pub add hive hive_flutter
# 可选:对象序列化生成器
flutter pub add dev:hive_generator dev:build_runner
核心概念/方法
概念/方法作用注意事项
BoxHive的数据库表,存储键值对一个Box对应一个本地文件
Hive.initFlutter()初始化Hive(Flutter专用)需在runApp前执行
Hive.registerAdapter()注册自定义对象适配器自定义对象必须添加@HiveType和@HiveField
ValueListenableBuilder监听Box变化自动刷新UI无需手动调用setState
put/get/delete/clear增删改查操作同步操作,性能优于SharedPreferences
案例代码

user.dart

import 'package:hive/hive.dart';

part 'user.g.dart';

@HiveType(typeId: 0)
class User extends HiveObject {
  @HiveField(0)
  late String name;

  @HiveField(1)
  late int age;

  @HiveField(2)
  late String email;

  User({required this.name, required this.age, required this.email});
}

user.g.dart

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user.dart';

// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************

class UserAdapter extends TypeAdapter<User> {
  @override
  final int typeId = 0;

  @override
  User read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return User(
      name: fields[0] as String,
      age: fields[1] as int,
      email: fields[2] as String,
    );
  }

  @override
  void write(BinaryWriter writer, User obj) {
    writer
      ..writeByte(3)
      ..writeByte(0)
      ..write(obj.name)
      ..writeByte(1)
      ..write(obj.age)
      ..writeByte(2)
      ..write(obj.email);
  }

  @override
  int get hashCode => typeId.hashCode;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is UserAdapter &&
          runtimeType == other.runtimeType &&
          typeId == other.typeId;
}
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'user.dart';

void main() async {
  // 2. 初始化Hive
  await Hive.initFlutter();
  // 注册适配器(自定义对象必须)
  Hive.registerAdapter(UserAdapter());
  // 打开盒子(数据库表)
  await Hive.openBox<User>('users');
  
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hive 示例',
      home: const HiveDemo(),
    );
  }
}

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

  @override
  State<HiveDemo> createState() => _HiveDemoState();
}

class _HiveDemoState extends State<HiveDemo> {
  final Box<User> _userBox = Hive.box<User>('users');
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _ageController = TextEditingController();
  final TextEditingController _emailController = TextEditingController();

  // 添加用户
  void _addUser() {
    final name = _nameController.text.trim();
    final ageText = _ageController.text.trim();
    final email = _emailController.text.trim();

    if (name.isEmpty || ageText.isEmpty || email.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请填写所有字段')),
      );
      return;
    }

    final age = int.tryParse(ageText);
    if (age == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('年龄必须是数字')),
      );
      return;
    }

    final user = User(
      name: name,
      age: age,
      email: email,
    );
    _userBox.add(user);
    _clearInputs();
  }

  // 删除用户
  void _deleteUser(int index) {
    _userBox.deleteAt(index);
  }

  void _clearInputs() {
    _nameController.clear();
    _ageController.clear();
    _emailController.clear();
  }

  @override
  void dispose() {
    _nameController.dispose();
    _ageController.dispose();
    _emailController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hive NoSQL 演示')),
      body: Column(
        children: [
          // 输入表单
          Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                TextField(
                  controller: _nameController,
                  decoration: const InputDecoration(hintText: '姓名'),
                ),
                TextField(
                  controller: _ageController,
                  decoration: const InputDecoration(hintText: '年龄'),
                  keyboardType: TextInputType.number,
                ),
                TextField(
                  controller: _emailController,
                  decoration: const InputDecoration(hintText: '邮箱'),
                ),
                const SizedBox(height: 10),
                ElevatedButton(
                  onPressed: _addUser,
                  child: const Text('添加用户'),
                ),
              ],
            ),
          ),
          // 列表展示
          Expanded(
            child: ValueListenableBuilder(
              // 监听盒子变化,自动刷新UI
              valueListenable: _userBox.listenable(),
              builder: (context, Box<User> box, child) {
                return ListView.builder(
                  itemCount: box.length,
                  itemBuilder: (context, index) {
                    final user = box.getAt(index)!;
                    return ListTile(
                      title: Text(user.name),
                      subtitle: Text('${user.age}岁 | ${user.email}'),
                      trailing: IconButton(
                        icon: const Icon(Icons.delete, color: Colors.red),
                        onPressed: () => _deleteUser(index),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

生成序列化代码

在终端执行:

flutter packages pub run build_runner build
注意事项
  1. TypeId 唯一性:每个自定义对象的 typeId 和字段的 HiveField 序号必须唯一;
  2. 加密支持:打开Box时可设置密码(encryptionCipher: HiveAesCipher(key)),加密存储数据;
  3. 性能优势:Hive是纯Dart实现,无原生桥接开销,读写速度远快于SharedPreferences;
  4. 持久化:数据实时写入磁盘,无需手动提交。

2.3 数据库存储:SQLite & Drift

核心概念
  • SQLite:跨平台的轻量级关系型数据库,Flutter 通过 sqflite 插件封装;
  • Drift(原Moor) :基于SQLite的现代化ORM框架,用Dart语法替代原生SQL,简化数据库操作。
前置依赖(Drift)
flutter pub add drift sqlite3_flutter_libs path_provider path
flutter pub add dev:drift_dev dev:build_runner
核心概念/方法
概念/方法作用注意事项
Table定义数据库表结构支持多种字段类型(int/text/bool/dateTime等)
DriftDatabase数据库核心类需指定包含的表,生成CRUD代码
select/into/update/delete数据库操作支持链式调用(如where/orderBy)
watch()监听数据变化,返回Stream实时更新UI,响应式编程
NativeDatabase原生SQLite数据库连接支持加密(需额外依赖)
案例代码
// db.dart
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

// 1. 定义表结构
class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 1, max: 50)();
  TextColumn get content => text()();
  BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
  DateTimeColumn get createTime => dateTime().withDefault(currentDateAndTime)();
}

// 2. 定义数据库类
part 'db.g.dart';

@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());

  // 数据库版本
  @override
  int get schemaVersion => 1;

  // CRUD操作
  // 获取所有待办
  Future<List<Todo>> getAllTodos() => select(todos).get();
  // 监听待办变化(实时更新)
  Stream<List<Todo>> watchAllTodos() => select(todos).watch();
  // 添加待办
  Future<int> addTodo(TodosCompanion todo) => into(todos).insert(todo);
  // 更新待办
  Future<bool> updateTodo(Todo todo) => update(todos).replace(todo);
  // 删除待办
  Future<int> deleteTodo(Todo todo) => delete(todos).delete(todo);
  // 标记完成/未完成
  Future<void> toggleTodo(Todo todo) async {
    await update(todos).replace(todo.copyWith(isCompleted: !todo.isCompleted));
  }
}

// 打开数据库连接
LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'todo_db.sqlite'));
    return NativeDatabase(file);
  });
}

// main.dart
import 'package:flutter/material.dart';
import 'package:drift/drift.dart';
import 'db.dart';

void main() async {
  // 初始化数据库
  final db = AppDatabase();
  runApp(MyApp(db: db));
}

class MyApp extends StatelessWidget {
  final AppDatabase db;
  const MyApp({super.key, required this.db});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Drift/SQLite 示例',
      home: TodoListPage(db: db),
    );
  }
}

class TodoListPage extends StatefulWidget {
  final AppDatabase db;
  const TodoListPage({super.key, required this.db});

  @override
  State<TodoListPage> createState() => _TodoListPageState();
}

class _TodoListPageState extends State<TodoListPage> {
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _contentController = TextEditingController();

  // 添加待办
  void _addTodo() {
    if (_titleController.text.isEmpty) return;
    final todo = TodosCompanion(
      title: Value(_titleController.text),
      content: Value(_contentController.text),
    );
    widget.db.addTodo(todo);
    _titleController.clear();
    _contentController.clear();
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('待办事项 (Drift/SQLite)')),
      body: Column(
        children: [
          // 输入表单
          Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                TextField(
                  controller: _titleController,
                  decoration: const InputDecoration(hintText: '待办标题'),
                ),
                TextField(
                  controller: _contentController,
                  decoration: const InputDecoration(hintText: '待办内容'),
                  maxLines: 2,
                ),
                const SizedBox(height: 10),
                ElevatedButton(
                  onPressed: _addTodo,
                  child: const Text('添加待办'),
                ),
              ],
            ),
          ),
          // 待办列表
          Expanded(
            child: StreamBuilder(
              stream: widget.db.watchAllTodos(),
              builder: (context, snapshot) {
                if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
                final todos = snapshot.data!;
                return ListView.builder(
                  itemCount: todos.length,
                  itemBuilder: (context, index) {
                    final todo = todos[index];
                    return ListTile(
                      title: Text(todo.title, style: TextStyle(
                        decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
                      )),
                      subtitle: Text('创建时间:${todo.createTime.toString().substring(0, 19)}'),
                      trailing: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          IconButton(
                            icon: Icon(
                              todo.isCompleted ? Icons.check_circle : Icons.circle_outlined,
                              color: todo.isCompleted ? Colors.green : null,
                            ),
                            onPressed: () => widget.db.toggleTodo(todo),
                          ),
                          IconButton(
                            icon: const Icon(Icons.delete, color: Colors.red),
                            onPressed: () => widget.db.deleteTodo(todo),
                          ),
                        ],
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
生成Drift代码
flutter packages pub run build_runner build
注意事项
  1. 数据库版本:表结构变更时需升级 schemaVersion 并编写迁移逻辑;
  2. 异步操作:所有数据库操作都是异步的,需用 Future/Stream 处理;
  3. 性能优化:大量数据查询时建议使用索引(indexed());
  4. 资源释放:应用退出时需关闭数据库连接(db.close())。

2.4 文件读写与应用目录

核心概念

Flutter 通过 dart:iopath_provider 插件实现文件读写,可访问应用专属的沙盒目录(避免权限问题),适合存储大文件(图片、日志、缓存等)。

前置依赖
flutter pub add path_provider
核心API/目录
API/目录作用注意事项
getApplicationDocumentsDirectory()获取应用文档目录(持久化)数据不会被系统自动清理
getTemporaryDirectory()获取临时目录(缓存)可能被系统清理,适合临时文件
File.writeAsString()写入字符串到文件支持编码格式(默认UTF-8)
File.readAsString()从文件读取字符串文件不存在时会抛出异常
jsonEncode/jsonDecodeJSON序列化/反序列化用于存储结构化数据
File.exists()判断文件是否存在读写前建议先判断
案例代码
import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '文件读写示例',
      home: const FileOperationsDemo(),
    );
  }
}

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

  @override
  State<FileOperationsDemo> createState() => _FileOperationsDemoState();
}

class _FileOperationsDemoState extends State<FileOperationsDemo> {
  String _fileContent = '暂无数据';
  final TextEditingController _textController = TextEditingController();

  // 获取应用目录
  Future<Directory> _getAppDirectory() async {
    // 文档目录:持久化存储,用户可见(部分平台)
    // return await getApplicationDocumentsDirectory();
    // 缓存目录:临时存储,可能被系统清理
    return await getTemporaryDirectory();
  }

  // 写入文件
  Future<void> _writeToFile() async {
    if (_textController.text.isEmpty) return;
    final dir = await _getAppDirectory();
    final file = File('${dir.path}/demo.txt');
    // 写入字符串
    await file.writeAsString(_textController.text);
    // 写入JSON示例
    // final data = {'name': 'Flutter', 'version': '3.16.0'};
    // await file.writeAsString(jsonEncode(data));
    _textController.clear();
    _readFromFile(); // 重新读取
  }

  // 读取文件
  Future<void> _readFromFile() async {
    final dir = await _getAppDirectory();
    final file = File('${dir.path}/demo.txt');
    if (await file.exists()) {
      final content = await file.readAsString();
      // 读取JSON示例
      // final data = jsonDecode(content);
      setState(() => _fileContent = content);
    } else {
      setState(() => _fileContent = '文件不存在');
    }
  }

  // 删除文件
  Future<void> _deleteFile() async {
    final dir = await _getAppDirectory();
    final file = File('${dir.path}/demo.txt');
    if (await file.exists()) {
      await file.delete();
      setState(() => _fileContent = '文件已删除');
    }
  }

  @override
  void initState() {
    super.initState();
    _readFromFile(); // 初始化读取
  }

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('文件读写演示')),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            TextField(
              controller: _textController,
              decoration: const InputDecoration(hintText: '请输入要保存的内容'),
              maxLines: 3,
            ),
            const SizedBox(height: 20),
            Row(
              children: [
                ElevatedButton(
                  onPressed: _writeToFile,
                  child: const Text('写入文件'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _readFromFile,
                  child: const Text('读取文件'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _deleteFile,
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                  child: const Text('删除文件'),
                ),
              ],
            ),
            const SizedBox(height: 30),
            const Text('文件内容:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 10),
            Expanded(
              child: SingleChildScrollView(
                child: Text(_fileContent),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
注意事项
  1. 权限问题:无需申请额外权限(沙盒目录内操作),访问外部存储需申请权限;
  2. 大文件处理:大文件建议使用 readAsBytes()/writeAsBytes(),避免内存溢出;
  3. 路径拼接:建议使用 path 插件的 join() 方法拼接路径,适配不同平台;
  4. 异常处理:文件操作需捕获异常(如磁盘满、权限不足)。

2.5 数据安全与加密

核心概念

本地存储的敏感数据(如用户token、密码、个人信息)需加密后存储,常用加密算法:

  • AES:对称加密,适合加密完整数据;
  • SHA-256:哈希算法,适合加密密码(不可逆);
  • HMAC:带密钥的哈希算法,防止数据篡改。
前置依赖
flutter pub add encrypt crypto
核心加密方法
方法作用适用场景
AES(encrypt插件)对称加密,可解密敏感数据(Token、用户信息)
SHA-256(crypto)哈希加密,不可逆密码存储(不存储明文)
HMAC带密钥的哈希,防篡改数据完整性校验
案例代码
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:encrypt/encrypt.dart';
import 'package:crypto/crypto.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '数据加密示例',
      home: const EncryptionDemo(),
    );
  }
}

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

  @override
  State<EncryptionDemo> createState() => _EncryptionDemoState();
}

class _EncryptionDemoState extends State<EncryptionDemo> {
  // 加密密钥(建议从服务端获取,或设备唯一标识生成)
  final String _secretKey = 'my_32_char_secret_key_12345678'; // AES-256 需要32位密钥
  final TextEditingController _sensitiveDataController = TextEditingController();
  String _encryptedData = '';
  String _decryptedData = '';
  String _hashedPassword = '';

  // AES加密
  String _aesEncrypt(String plainText) {
    final key = Key.fromUtf8(_secretKey);
    final iv = IV.fromLength(16); // 16位初始化向量
    final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
    final encrypted = encrypter.encrypt(plainText, iv: iv);
    return encrypted.base64;
  }

  // AES解密
  String _aesDecrypt(String encryptedText) {
    final key = Key.fromUtf8(_secretKey);
    final iv = IV.fromLength(16);
    final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
    final decrypted = encrypter.decrypt(Encrypted.fromBase64(encryptedText), iv: iv);
    return decrypted;
  }

  // SHA-256哈希(密码加密)
  String _sha256Hash(String password) {
    final bytes = utf8.encode(password);
    final digest = sha256.convert(bytes);
    return digest.toString();
  }

  // 加密并保存到SharedPreferences
  Future<void> _encryptAndSave() async {
    final plainText = _sensitiveDataController.text;
    if (plainText.isEmpty) return;
    
    // 加密
    final encrypted = _aesEncrypt(plainText);
    // 哈希密码示例
    final hashedPwd = _sha256Hash('user_password_123');
    
    // 保存加密后的数据
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('encrypted_token', encrypted);
    
    setState(() {
      _encryptedData = encrypted;
      _hashedPassword = hashedPwd;
    });
  }

  // 从SharedPreferences读取并解密
  Future<void> _loadAndDecrypt() async {
    final prefs = await SharedPreferences.getInstance();
    final encrypted = prefs.getString('encrypted_token') ?? '';
    if (encrypted.isEmpty) {
      setState(() => _decryptedData = '无加密数据');
      return;
    }
    
    // 解密
    final decrypted = _aesDecrypt(encrypted);
    setState(() => _decryptedData = decrypted);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('数据安全与加密')),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              controller: _sensitiveDataController,
              decoration: const InputDecoration(hintText: '输入敏感数据(如Token)'),
            ),
            const SizedBox(height: 20),
            Row(
              children: [
                ElevatedButton(
                  onPressed: _encryptAndSave,
                  child: const Text('加密并保存'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _loadAndDecrypt,
                  child: const Text('读取并解密'),
                ),
              ],
            ),
            const SizedBox(height: 30),
            const Text('加密后的数据:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text(_encryptedData),
            const SizedBox(height: 20),
            const Text('解密后的数据:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text(_decryptedData),
            const SizedBox(height: 20),
            const Text('密码SHA-256哈希:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text(_hashedPassword),
          ],
        ),
      ),
    );
  }
}
注意事项
  1. 密钥安全:密钥不要硬编码在代码中,建议从服务端获取或通过设备信息生成;
  2. AES模式:推荐使用CBC/GCM模式,IV(初始化向量)需随机生成并和密文一起存储;
  3. 哈希加盐:密码哈希时建议添加随机盐(salt),防止彩虹表攻击;
  4. 加密范围:仅加密敏感数据,普通数据无需加密(影响性能)。

三、综合应用案例:本地存储全方案电商APP

功能说明

整合本章所有技术,实现一个电商APP的本地存储方案:

  1. SharedPreferences:存储用户偏好设置(暗黑模式、语言)、登录状态(加密token);
  2. Hive:存储用户收藏的商品列表(自定义对象、加密);
  3. Drift/SQLite:存储本地订单记录(关系型数据);
  4. 文件存储:保存用户头像(图片文件)、APP日志;
  5. 数据加密:加密存储token、用户信息,密码哈希存储。

完整代码(核心整合版)

// 第一步:配置依赖(pubspec.yaml)
/*
dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.2
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  drift: ^2.14.0
  sqlite3_flutter_libs: ^0.5.18
  path_provider: ^2.1.2
  path: ^1.8.3
  encrypt: ^5.0.1
  crypto: ^3.0.3
  image_picker: ^1.0.4 # 用于选择头像

dev_dependencies:
  hive_generator: ^2.0.1
  drift_dev: ^2.14.0
  build_runner: ^2.4.8
*/

// 第二步:定义数据模型和数据库
import 'dart:io';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:encrypt/encrypt.dart';
import 'package:crypto/crypto.dart';
import 'package:image_picker/image_picker.dart';

// ---------------------- 1. 加密工具类 ----------------------
class EncryptionUtil {
  static final String _secretKey = 'ecommerce_32_char_key_1234567890'; // 32位密钥
  static final IV _iv = IV.fromLength(16);

  // AES加密
  static String encryptAES(String plainText) {
    final key = Key.fromUtf8(_secretKey);
    final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
    final encrypted = encrypter.encrypt(plainText, iv: _iv);
    return encrypted.base64;
  }

  // AES解密
  static String decryptAES(String encryptedText) {
    final key = Key.fromUtf8(_secretKey);
    final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
    final decrypted = encrypter.decrypt(Encrypted.fromBase64(encryptedText), iv: _iv);
    return decrypted;
  }

  // SHA-256哈希
  static String sha256Hash(String input) {
    final bytes = utf8.encode(input);
    final digest = sha256.convert(bytes);
    return digest.toString();
  }
}

// ---------------------- 2. Hive 商品模型 ----------------------
part 'product.g.dart';

@HiveType(typeId: 0)
class Product extends HiveObject {
  @HiveField(0)
  late String id;

  @HiveField(1)
  late String name;

  @HiveField(2)
  late double price;

  @HiveField(3)
  late String imageUrl;

  Product({required this.id, required this.name, required this.price, required this.imageUrl});
}

// ---------------------- 3. Drift 订单数据库 ----------------------
class Orders extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get orderId => text()();
  TextColumn get productIds => text()(); // 商品ID列表,逗号分隔
  RealColumn get totalPrice => real()();
  DateTimeColumn get createTime => dateTime().withDefault(currentDateAndTime)();
}

part 'order_db.g.dart';

@DriftDatabase(tables: [Orders])
class OrderDatabase extends _$OrderDatabase {
  OrderDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;

  // 订单CRUD
  Future<int> addOrder(OrdersCompanion order) => into(orders).insert(order);
  Future<List<Order>> getAllOrders() => select(orders).get();
  Stream<List<Order>> watchAllOrders() => select(orders).watch();
}

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'orders_db.sqlite'));
    return NativeDatabase(file);
  });
}

// ---------------------- 4. 主应用 ----------------------
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 初始化SharedPreferences
  final prefs = await SharedPreferences.getInstance();

  // 初始化Hive(加密存储收藏商品)
  await Hive.initFlutter();
  Hive.registerAdapter(ProductAdapter());
  final encryptionKey = EncryptionUtil.sha256Hash('hive_secret_key').substring(0, 32);
  final cipher = HiveAesCipher(utf8.encode(encryptionKey) as Uint8List);
  await Hive.openBox<Product>('favorite_products', encryptionCipher: cipher);

  // 初始化Drift数据库
  final orderDb = OrderDatabase();

  runApp(MyEcommerceApp(prefs: prefs, orderDb: orderDb));
}

class MyEcommerceApp extends StatelessWidget {
  final SharedPreferences prefs;
  final OrderDatabase orderDb;
  const MyEcommerceApp({super.key, required this.prefs, required this.orderDb});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '电商APP本地存储示例',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: prefs.getBool('dark_mode') ?? false ? Brightness.dark : Brightness.light,
      ),
      home: MainPage(prefs: prefs, orderDb: orderDb),
    );
  }
}

// ---------------------- 5. 主页面 ----------------------
class MainPage extends StatefulWidget {
  final SharedPreferences prefs;
  final OrderDatabase orderDb;
  const MainPage({super.key, required this.prefs, required this.orderDb});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentTab = 0;
  final Box<Product> _favoriteBox = Hive.box<Product>('favorite_products');
  final ImagePicker _imagePicker = ImagePicker();
  String _avatarPath = '';

  // 模拟商品数据
  final List<Product> _products = [    Product(id: '1', name: 'Flutter 实战教程', price: 59.9, imageUrl: 'https://example.com/1.jpg'),    Product(id: '2', name: 'Dart 进阶指南', price: 49.9, imageUrl: 'https://example.com/2.jpg'),  ];

  @override
  void initState() {
    super.initState();
    // 读取用户头像路径
    _avatarPath = widget.prefs.getString('avatar_path') ?? '';
    // 初始化登录状态(加密存储token)
    if (!widget.prefs.containsKey('user_token')) {
      final token = EncryptionUtil.encryptAES('user_123_token_xyz');
      widget.prefs.setString('user_token', token);
    }
  }

  // 切换暗黑模式
  void _toggleDarkMode(bool value) {
    widget.prefs.setBool('dark_mode', value);
    setState(() {
      // 重启应用生效(简化示例,实际可使用Provider)
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => MainPage(prefs: widget.prefs, orderDb: widget.orderDb)),
      );
    });
  }

  // 添加收藏商品
  void _addToFavorites(Product product) {
    _favoriteBox.add(product);
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('${product.name} 已加入收藏')),
      );
    }
  }

  // 下单
  void _createOrder() async {
    final order = OrdersCompanion(
      orderId: Value('ORD${DateTime.now().millisecondsSinceEpoch}'),
      productIds: Value(_products.map((p) => p.id).join(',')),
      totalPrice: Value(_products.fold(0.0, (sum, p) => sum + p.price)),
    );
    await widget.orderDb.addOrder(order);
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('订单创建成功')),
      );
    }
  }

  // 选择并保存头像
  Future<void> _pickAvatar() async {
    final XFile? image = await _imagePicker.pickImage(source: ImageSource.gallery);
    if (image == null) return;
    
    // 保存头像路径
    await widget.prefs.setString('avatar_path', image.path);
    setState(() => _avatarPath = image.path);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('电商APP')),
      body: IndexedStack(
        index: _currentTab,
        children: [
          // 首页(商品列表)
          ListView.builder(
            itemCount: _products.length,
            itemBuilder: (context, index) {
              final product = _products[index];
              return ListTile(
                title: Text(product.name),
                subtitle: Text('¥${product.price}'),
                trailing: IconButton(
                  icon: const Icon(Icons.favorite_border),
                  onPressed: () => _addToFavorites(product),
                ),
              );
            },
          ),
          // 收藏页面
          ValueListenableBuilder(
            valueListenable: _favoriteBox.listenable(),
            builder: (context, Box<Product> box, child) {
              if (box.isEmpty) return const Center(child: Text('暂无收藏商品'));
              return ListView.builder(
                itemCount: box.length,
                itemBuilder: (context, index) {
                  final product = box.getAt(index)!;
                  return ListTile(
                    title: Text(product.name),
                    subtitle: Text('¥${product.price}'),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete, color: Colors.red),
                      onPressed: () => box.deleteAt(index),
                    ),
                  );
                },
              );
            },
          ),
          // 订单页面
          StreamBuilder(
            stream: widget.orderDb.watchAllOrders(),
            builder: (context, snapshot) {
              if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
              final orders = snapshot.data!;
              if (orders.isEmpty) return const Center(child: Text('暂无订单'));
              return ListView.builder(
                itemCount: orders.length,
                itemBuilder: (context, index) {
                  final order = orders[index];
                  return ListTile(
                    title: Text('订单号:${order.orderId}'),
                    subtitle: Text('总价:¥${order.totalPrice} | ${order.createTime.toString().substring(0, 10)}'),
                  );
                },
              );
            },
          ),
          // 我的页面
          Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                // 头像
                GestureDetector(
                  onTap: _pickAvatar,
                  child: CircleAvatar(
                    radius: 50,
                    backgroundImage: _avatarPath.isNotEmpty ? FileImage(File(_avatarPath)) : null,
                    child: _avatarPath.isEmpty ? const Icon(Icons.person, size: 50) : null,
                  ),
                ),
                const SizedBox(height: 20),
                // 暗黑模式
                SwitchListTile(
                  title: const Text('暗黑模式'),
                  value: widget.prefs.getBool('dark_mode') ?? false,
                  onChanged: _toggleDarkMode,
                ),
                const SizedBox(height: 20),
                // 下单按钮
                ElevatedButton(
                  onPressed: _createOrder,
                  child: const Text('创建测试订单'),
                ),
                const SizedBox(height: 20),
                // 解密Token
                ElevatedButton(
                  onPressed: () {
                    final encryptedToken = widget.prefs.getString('user_token')!;
                    final decryptedToken = EncryptionUtil.decryptAES(encryptedToken);
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('解密Token:$decryptedToken')),
                    );
                  },
                  child: const Text('查看解密Token'),
                ),
              ],
            ),
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentTab,
        onTap: (index) => setState(() => _currentTab = index),
        type: BottomNavigationBarType.fixed,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.favorite), label: '收藏'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_bag), label: '订单'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

功能验证步骤

  1. SharedPreferences

    1. 切换“暗黑模式” → 重启页面生效;
    2. 查看“解密Token” → 展示加密存储的用户token;
    3. 选择头像 → 保存头像路径并展示。
  2. Hive

    1. 首页点击商品收藏按钮 → 收藏页面展示收藏的商品;
    2. 删除收藏商品 → 列表实时更新。
  3. Drift/SQLite

    1. 点击“创建测试订单” → 订单页面展示新创建的订单;
    2. 订单数据实时监听,自动刷新。
  4. 数据加密

    1. Hive的收藏商品使用AES加密存储;
    2. 用户token加密后存储在SharedPreferences;
    3. 密码使用SHA-256哈希(示例)。

关键注意事项

  • 所有本地存储方案最终都依赖设备文件系统,需注意磁盘空间和权限;
  • 加密是敏感数据的必要操作,避免明文存储用户信息;
  • 数据库和Hive需正确初始化和关闭,避免资源泄漏;
  • 跨平台路径处理使用path插件,避免平台兼容性问题。