第11章 - 本地存储
嗨,朋友!我是长安。
App 经常需要在本地保存一些数据,比如用户设置、登录状态、缓存数据等。这一章,我们来学习 Flutter 中的本地存储方案。
📦 本地存储方案对比
| 方案 | 适用场景 | 特点 |
|---|---|---|
| SharedPreferences | 简单键值对(设置、Token) | 轻量、简单 |
| 文件存储 | 文本、JSON、图片 | 灵活、适合大数据 |
| SQLite | 结构化数据、复杂查询 | 关系型数据库 |
| Hive | 快速键值存储 | NoSQL、速度快 |
🔑 SharedPreferences
SharedPreferences 适合存储简单的键值对数据,如用户设置、登录状态、主题偏好等。
1. 添加依赖
dependencies:
shared_preferences: ^2.2.2
2. 基本用法
import 'package:shared_preferences/shared_preferences.dart';
// 保存数据
Future<void> saveData() async {
final prefs = await SharedPreferences.getInstance();
// 保存不同类型的数据
await prefs.setString('username', '长安');
await prefs.setInt('age', 25);
await prefs.setDouble('score', 95.5);
await prefs.setBool('isLogin', true);
await prefs.setStringList('tags', ['Flutter', 'Dart', 'Mobile']);
}
// 读取数据
Future<void> loadData() async {
final prefs = await SharedPreferences.getInstance();
// 读取数据(提供默认值)
final username = prefs.getString('username') ?? '未知';
final age = prefs.getInt('age') ?? 0;
final score = prefs.getDouble('score') ?? 0.0;
final isLogin = prefs.getBool('isLogin') ?? false;
final tags = prefs.getStringList('tags') ?? [];
print('用户名: $username, 年龄: $age');
}
// 删除数据
Future<void> removeData() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('username'); // 删除指定键
await prefs.clear(); // 清空所有数据
}
// 检查键是否存在
Future<void> checkKey() async {
final prefs = await SharedPreferences.getInstance();
final hasKey = prefs.containsKey('username');
print('是否存在 username: $hasKey');
}
3. 实战:用户设置页面
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool _darkMode = false;
bool _notifications = true;
String _language = '中文';
void initState() {
super.initState();
_loadSettings();
}
// 加载设置
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_darkMode = prefs.getBool('darkMode') ?? false;
_notifications = prefs.getBool('notifications') ?? true;
_language = prefs.getString('language') ?? '中文';
});
}
// 保存设置
Future<void> _saveSetting(String key, dynamic value) async {
final prefs = await SharedPreferences.getInstance();
if (value is bool) {
await prefs.setBool(key, value);
} else if (value is String) {
await prefs.setString(key, value);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
SwitchListTile(
title: const Text('深色模式'),
subtitle: const Text('使用深色主题'),
value: _darkMode,
onChanged: (value) {
setState(() => _darkMode = value);
_saveSetting('darkMode', value);
},
),
SwitchListTile(
title: const Text('推送通知'),
subtitle: const Text('接收消息通知'),
value: _notifications,
onChanged: (value) {
setState(() => _notifications = value);
_saveSetting('notifications', value);
},
),
ListTile(
title: const Text('语言'),
subtitle: Text(_language),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () async {
final result = await showDialog<String>(
context: context,
builder: (context) => SimpleDialog(
title: const Text('选择语言'),
children: ['中文', 'English', '日本語'].map((lang) {
return SimpleDialogOption(
onPressed: () => Navigator.pop(context, lang),
child: Text(lang),
);
}).toList(),
),
);
if (result != null) {
setState(() => _language = result);
_saveSetting('language', result);
}
},
),
],
),
);
}
}
4. 封装 SharedPreferences 工具类
class StorageUtil {
static SharedPreferences? _prefs;
// 初始化(在 main 中调用)
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// Token
static String? get token => _prefs?.getString('token');
static Future<void> setToken(String? value) async {
if (value == null) {
await _prefs?.remove('token');
} else {
await _prefs?.setString('token', value);
}
}
// 用户 ID
static int? get userId => _prefs?.getInt('userId');
static Future<void> setUserId(int? value) async {
if (value == null) {
await _prefs?.remove('userId');
} else {
await _prefs?.setInt('userId', value);
}
}
// 是否首次启动
static bool get isFirstLaunch => _prefs?.getBool('isFirstLaunch') ?? true;
static Future<void> setFirstLaunch(bool value) async {
await _prefs?.setBool('isFirstLaunch', value);
}
// 深色模式
static bool get isDarkMode => _prefs?.getBool('isDarkMode') ?? false;
static Future<void> setDarkMode(bool value) async {
await _prefs?.setBool('isDarkMode', value);
}
// 清除所有数据(退出登录时调用)
static Future<void> clear() async {
await _prefs?.clear();
}
}
// 在 main.dart 中初始化
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await StorageUtil.init();
runApp(const MyApp());
}
// 使用
StorageUtil.setToken('abc123');
print(StorageUtil.token);
📁 文件存储
对于较大的数据或需要保存文件的场景,可以使用文件存储。
1. 添加依赖
dependencies:
path_provider: ^2.1.1
2. 获取存储路径
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<void> getPaths() async {
// 应用文档目录(持久化存储,不会被清理)
final docDir = await getApplicationDocumentsDirectory();
print('文档目录: ${docDir.path}');
// 临时目录(可能被系统清理)
final tempDir = await getTemporaryDirectory();
print('临时目录: ${tempDir.path}');
// 应用支持目录
final supportDir = await getApplicationSupportDirectory();
print('支持目录: ${supportDir.path}');
// 缓存目录
final cacheDir = await getApplicationCacheDirectory();
print('缓存目录: ${cacheDir.path}');
}
3. 读写文本文件
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class FileStorage {
// 获取文件路径
static Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
// 获取文件
static Future<File> _getFile(String filename) async {
final path = await _localPath;
return File('$path/$filename');
}
// 写入文件
static Future<void> writeFile(String filename, String content) async {
final file = await _getFile(filename);
await file.writeAsString(content);
}
// 读取文件
static Future<String> readFile(String filename) async {
try {
final file = await _getFile(filename);
return await file.readAsString();
} catch (e) {
return '';
}
}
// 删除文件
static Future<void> deleteFile(String filename) async {
try {
final file = await _getFile(filename);
await file.delete();
} catch (e) {
print('删除失败: $e');
}
}
// 检查文件是否存在
static Future<bool> fileExists(String filename) async {
final file = await _getFile(filename);
return await file.exists();
}
}
// 使用示例
await FileStorage.writeFile('note.txt', '这是一段笔记内容');
final content = await FileStorage.readFile('note.txt');
print(content);
4. 读写 JSON 文件
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class JsonStorage {
static Future<File> _getFile(String filename) async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$filename');
}
// 保存 JSON 数据
static Future<void> saveJson(String filename, Map<String, dynamic> data) async {
final file = await _getFile(filename);
final jsonString = json.encode(data);
await file.writeAsString(jsonString);
}
// 读取 JSON 数据
static Future<Map<String, dynamic>?> loadJson(String filename) async {
try {
final file = await _getFile(filename);
if (await file.exists()) {
final jsonString = await file.readAsString();
return json.decode(jsonString);
}
} catch (e) {
print('读取 JSON 失败: $e');
}
return null;
}
// 保存列表数据
static Future<void> saveList(String filename, List<Map<String, dynamic>> data) async {
final file = await _getFile(filename);
final jsonString = json.encode(data);
await file.writeAsString(jsonString);
}
// 读取列表数据
static Future<List<Map<String, dynamic>>> loadList(String filename) async {
try {
final file = await _getFile(filename);
if (await file.exists()) {
final jsonString = await file.readAsString();
final List<dynamic> jsonData = json.decode(jsonString);
return jsonData.cast<Map<String, dynamic>>();
}
} catch (e) {
print('读取列表失败: $e');
}
return [];
}
}
// 使用示例:保存用户信息
await JsonStorage.saveJson('user.json', {
'id': 1,
'name': '长安',
'email': 'changan@example.com',
});
// 读取用户信息
final user = await JsonStorage.loadJson('user.json');
print('用户名: ${user?['name']}');
// 保存待办列表
await JsonStorage.saveList('todos.json', [
{'id': 1, 'title': '学习 Flutter', 'done': false},
{'id': 2, 'title': '写文档', 'done': true},
]);
🗃️ SQLite 数据库
对于需要结构化存储和复杂查询的场景,使用 SQLite。
1. 添加依赖
dependencies:
sqflite: ^2.3.0
path: ^1.8.3
2. 数据库帮助类
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static Database? _database;
static const String _dbName = 'app_database.db';
static const int _dbVersion = 1;
// 获取数据库实例
static Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
// 初始化数据库
static Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, _dbName);
return await openDatabase(
path,
version: _dbVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
// 创建表
static Future<void> _onCreate(Database db, int version) async {
// 创建用户表
await db.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
avatar TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''');
// 创建待办表
await db.execute('''
CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
is_done INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''');
}
// 升级数据库
static Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// 数据库升级逻辑
if (oldVersion < 2) {
// 添加新列等
}
}
// 关闭数据库
static Future<void> close() async {
final db = await database;
await db.close();
_database = null;
}
}
3. 数据访问对象(DAO)
// 待办事项模型
class Todo {
final int? id;
final String title;
final String? description;
final bool isDone;
final DateTime? createdAt;
Todo({
this.id,
required this.title,
this.description,
this.isDone = false,
this.createdAt,
});
// 从 Map 创建
factory Todo.fromMap(Map<String, dynamic> map) {
return Todo(
id: map['id'],
title: map['title'],
description: map['description'],
isDone: map['is_done'] == 1,
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: null,
);
}
// 转换为 Map
Map<String, dynamic> toMap() {
return {
'title': title,
'description': description,
'is_done': isDone ? 1 : 0,
};
}
}
// 待办事项 DAO
class TodoDao {
// 插入
static Future<int> insert(Todo todo) async {
final db = await DatabaseHelper.database;
return await db.insert('todos', todo.toMap());
}
// 查询所有
static Future<List<Todo>> getAll() async {
final db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'todos',
orderBy: 'created_at DESC',
);
return maps.map((map) => Todo.fromMap(map)).toList();
}
// 根据 ID 查询
static Future<Todo?> getById(int id) async {
final db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'todos',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return Todo.fromMap(maps.first);
}
return null;
}
// 查询未完成的
static Future<List<Todo>> getIncomplete() async {
final db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'todos',
where: 'is_done = ?',
whereArgs: [0],
orderBy: 'created_at DESC',
);
return maps.map((map) => Todo.fromMap(map)).toList();
}
// 更新
static Future<int> update(Todo todo) async {
final db = await DatabaseHelper.database;
return await db.update(
'todos',
todo.toMap(),
where: 'id = ?',
whereArgs: [todo.id],
);
}
// 切换完成状态
static Future<int> toggleDone(int id, bool isDone) async {
final db = await DatabaseHelper.database;
return await db.update(
'todos',
{'is_done': isDone ? 1 : 0},
where: 'id = ?',
whereArgs: [id],
);
}
// 删除
static Future<int> delete(int id) async {
final db = await DatabaseHelper.database;
return await db.delete(
'todos',
where: 'id = ?',
whereArgs: [id],
);
}
// 删除所有已完成的
static Future<int> deleteCompleted() async {
final db = await DatabaseHelper.database;
return await db.delete(
'todos',
where: 'is_done = ?',
whereArgs: [1],
);
}
// 获取待办数量
static Future<int> getCount() async {
final db = await DatabaseHelper.database;
final result = await db.rawQuery('SELECT COUNT(*) as count FROM todos');
return result.first['count'] as int;
}
}
4. 在页面中使用
class TodoListPage extends StatefulWidget {
const TodoListPage({super.key});
State<TodoListPage> createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> {
List<Todo> _todos = [];
final TextEditingController _controller = TextEditingController();
void initState() {
super.initState();
_loadTodos();
}
Future<void> _loadTodos() async {
final todos = await TodoDao.getAll();
setState(() {
_todos = todos;
});
}
Future<void> _addTodo() async {
if (_controller.text.trim().isEmpty) return;
await TodoDao.insert(Todo(title: _controller.text.trim()));
_controller.clear();
await _loadTodos();
}
Future<void> _toggleTodo(Todo todo) async {
await TodoDao.toggleDone(todo.id!, !todo.isDone);
await _loadTodos();
}
Future<void> _deleteTodo(int id) async {
await TodoDao.delete(id);
await _loadTodos();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('待办事项')),
body: Column(
children: [
// 输入框
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: '输入待办事项...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: _addTodo,
child: const Text('添加'),
),
],
),
),
// 列表
Expanded(
child: _todos.isEmpty
? const Center(child: Text('暂无待办事项'))
: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return ListTile(
leading: Checkbox(
value: todo.isDone,
onChanged: (_) => _toggleTodo(todo),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isDone
? TextDecoration.lineThrough
: null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _deleteTodo(todo.id!),
),
);
},
),
),
],
),
);
}
}
📝 小结
这一章我们学习了:
- ✅ SharedPreferences - 简单键值对存储
- ✅ 文件存储 - 文本和 JSON 文件
- ✅ SQLite - 结构化数据库存储
- ✅ 各种存储方案的封装和最佳实践
存储方案选择指南
| 数据类型 | 推荐方案 |
|---|---|
| 用户设置、Token | SharedPreferences |
| 简单的缓存数据 | JSON 文件 |
| 结构化数据、需要查询 | SQLite |
| 大量非结构化数据 | 文件存储 |
💪 练习题
- 使用 SharedPreferences 实现一个「记住密码」功能
- 使用文件存储实现一个简单的笔记本应用
- 使用 SQLite 实现一个完整的待办事项应用
🚀 下一步
学会了本地存储后,下一章我们来学习 对话框与反馈,提升用户体验!
由 编程指南 提供
