前言:
这一章学完,通用的90%功能都能开发了,也就是说,已经可以开始vibecoding写代码了,在这个过程中,除了模型的代码外,你一定要学会排查问题,这是你学习这些知识点的目的。通过熟悉业务、熟悉需求,去控制生成代码,在深入细节处,生成代码或许就不足了,这个时候,你需要在细微处指导他怎么操作,就跟教导婴儿一样,你会发现,这十五讲的内容走下去,你已经具备了发现问题的内容。
至于深入能力,在你不断解决问题中就积累了。
一、总览
本讲聚焦 Flutter 应用中本地数据持久化的核心技术,解决“应用重启后数据不丢失”的问题。在移动开发中,本地存储是实现用户偏好设置、离线数据缓存、本地数据库等功能的基础。
-
存储方案选型:
- 简单键值对(设置、状态)→ SharedPreferences;
- 中等规模结构化数据(收藏、缓存)→ Hive;
- 复杂关系型数据(订单、用户)→ Drift/SQLite;
- 大文件(图片、日志)→ 文件读写。
-
数据安全原则:
- 敏感数据(Token、密码)必须加密存储;
- 密码使用不可逆哈希(SHA-256+盐),不存储明文;
- 密钥不要硬编码,建议动态获取。
-
性能与最佳实践:
- Hive性能优于SharedPreferences,适合高频读写;
- Drift/SQLite适合复杂查询和关系型数据;
- 文件操作需处理异常,大文件分块读写。
- 本讲解决的核心问题:如何在 Flutter 应用中安全、高效地将数据保存到设备本地,以及如何读取、更新、删除这些数据;
- 技术选型逻辑:根据数据类型(键值对/结构化数据/文件)、性能要求、复杂度选择对应的存储方案;
- 应用场景:用户登录状态保存、APP 主题/语言设置、离线商品列表、本地日志文件、加密存储敏感信息(如token)等。
Flutter 本地存储的底层是对原生平台(Android/iOS)存储能力的封装,核心原理结构如下:
-
跨平台封装:Flutter 本地存储方案本质是对 Android/iOS 原生存储API的封装,保证跨平台一致性;
-
存储介质:所有本地存储最终都落地到设备的文件系统(沙盒目录),不同方案只是数据组织和读写方式不同;
-
分层设计:应用层通过抽象层调用存储API,加密层可对敏感数据进行加密后再存储;
-
数据形态:
- 键值对存储(SharedPreferences/Hive):适合简单数据;
- 结构化存储(SQLite/Drift):适合复杂关系型数据;
- 文件存储:适合大文件(图片、日志、二进制数据)。
二、核心知识点
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('清空数据'),
),
],
),
],
),
),
);
}
}
注意事项
- 异步操作:所有读写操作依赖
SharedPreferences.getInstance()的异步结果,需用async/await; - 数据限制:仅支持基础数据类型,不支持自定义对象(需手动序列化/反序列化);
- 性能问题:不适合存储大量数据,频繁读写会影响性能;
- 持久化时机:
set方法调用后数据会异步写入磁盘,并非立即持久化。
2.2 NoSQL 存储:Hive
核心概念
Hive 是 Flutter 专属的轻量级 NoSQL 键值数据库,无需原生依赖,支持自定义对象、索引、加密,性能优于 SharedPreferences,适合存储中等规模的结构化数据。
前置依赖
flutter pub add hive hive_flutter
# 可选:对象序列化生成器
flutter pub add dev:hive_generator dev:build_runner
核心概念/方法
| 概念/方法 | 作用 | 注意事项 |
|---|---|---|
Box | Hive的数据库表,存储键值对 | 一个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
注意事项
- TypeId 唯一性:每个自定义对象的
typeId和字段的HiveField序号必须唯一; - 加密支持:打开Box时可设置密码(
encryptionCipher: HiveAesCipher(key)),加密存储数据; - 性能优势:Hive是纯Dart实现,无原生桥接开销,读写速度远快于SharedPreferences;
- 持久化:数据实时写入磁盘,无需手动提交。
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
注意事项
- 数据库版本:表结构变更时需升级
schemaVersion并编写迁移逻辑; - 异步操作:所有数据库操作都是异步的,需用
Future/Stream处理; - 性能优化:大量数据查询时建议使用索引(
indexed()); - 资源释放:应用退出时需关闭数据库连接(
db.close())。
2.4 文件读写与应用目录
核心概念
Flutter 通过 dart:io 和 path_provider 插件实现文件读写,可访问应用专属的沙盒目录(避免权限问题),适合存储大文件(图片、日志、缓存等)。
前置依赖
flutter pub add path_provider
核心API/目录
| API/目录 | 作用 | 注意事项 |
|---|---|---|
getApplicationDocumentsDirectory() | 获取应用文档目录(持久化) | 数据不会被系统自动清理 |
getTemporaryDirectory() | 获取临时目录(缓存) | 可能被系统清理,适合临时文件 |
File.writeAsString() | 写入字符串到文件 | 支持编码格式(默认UTF-8) |
File.readAsString() | 从文件读取字符串 | 文件不存在时会抛出异常 |
jsonEncode/jsonDecode | JSON序列化/反序列化 | 用于存储结构化数据 |
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),
),
),
],
),
),
);
}
}
注意事项
- 权限问题:无需申请额外权限(沙盒目录内操作),访问外部存储需申请权限;
- 大文件处理:大文件建议使用
readAsBytes()/writeAsBytes(),避免内存溢出; - 路径拼接:建议使用
path插件的join()方法拼接路径,适配不同平台; - 异常处理:文件操作需捕获异常(如磁盘满、权限不足)。
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),
],
),
),
);
}
}
注意事项
- 密钥安全:密钥不要硬编码在代码中,建议从服务端获取或通过设备信息生成;
- AES模式:推荐使用CBC/GCM模式,IV(初始化向量)需随机生成并和密文一起存储;
- 哈希加盐:密码哈希时建议添加随机盐(salt),防止彩虹表攻击;
- 加密范围:仅加密敏感数据,普通数据无需加密(影响性能)。
三、综合应用案例:本地存储全方案电商APP
功能说明
整合本章所有技术,实现一个电商APP的本地存储方案:
- SharedPreferences:存储用户偏好设置(暗黑模式、语言)、登录状态(加密token);
- Hive:存储用户收藏的商品列表(自定义对象、加密);
- Drift/SQLite:存储本地订单记录(关系型数据);
- 文件存储:保存用户头像(图片文件)、APP日志;
- 数据加密:加密存储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: '我的'),
],
),
);
}
}
功能验证步骤
-
SharedPreferences:
- 切换“暗黑模式” → 重启页面生效;
- 查看“解密Token” → 展示加密存储的用户token;
- 选择头像 → 保存头像路径并展示。
-
Hive:
- 首页点击商品收藏按钮 → 收藏页面展示收藏的商品;
- 删除收藏商品 → 列表实时更新。
-
Drift/SQLite:
- 点击“创建测试订单” → 订单页面展示新创建的订单;
- 订单数据实时监听,自动刷新。
-
数据加密:
- Hive的收藏商品使用AES加密存储;
- 用户token加密后存储在SharedPreferences;
- 密码使用SHA-256哈希(示例)。
关键注意事项
- 所有本地存储方案最终都依赖设备文件系统,需注意磁盘空间和权限;
- 加密是敏感数据的必要操作,避免明文存储用户信息;
- 数据库和Hive需正确初始化和关闭,避免资源泄漏;
- 跨平台路径处理使用
path插件,避免平台兼容性问题。