第 9 章 数据持久化(两个完整示例)
9.1 shared_preferences:记住一个值(设置项、token)
创建步骤
flutter create prefs_demo && cd prefs_demo;flutter pub add shared_preferences;- 替换
lib/main.dart,运行后点「+1」,杀掉 App 重进,数字还在。
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(const MaterialApp(home: PrefsDemoPage()));
class PrefsDemoPage extends StatefulWidget {
const PrefsDemoPage({super.key});
@override
State<PrefsDemoPage> createState() => _PrefsDemoPageState();
}
class _PrefsDemoPageState extends State<PrefsDemoPage> {
static const _key = 'launch_count';
int _count = 0;
bool _loaded = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
if (!mounted) return;
setState(() {
_count = prefs.getInt(_key) ?? 0;
_loaded = true;
});
}
Future<void> _increment() async {
final prefs = await SharedPreferences.getInstance();
final next = _count + 1;
await prefs.setInt(_key, next); // 先写盘,再更新界面
if (!mounted) return;
setState(() => _count = next);
}
Future<void> _reset() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key);
if (!mounted) return;
setState(() => _count = 0);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('本地存储')),
body: Center(
child: _loaded
? Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('累计点击次数'),
Text('$_count',
style: const TextStyle(
fontSize: 48, fontWeight: FontWeight.w800)),
const SizedBox(height: 20),
FilledButton(
onPressed: _increment, child: const Text('+1')),
TextButton(onPressed: _reset, child: const Text('清零')),
const SizedBox(height: 30),
const Text('杀掉 App 重新打开,数字依然在',
style: TextStyle(fontSize: 12, color: Colors.grey)),
],
)
: const CircularProgressIndicator(),
),
);
}
}
关键点
- 只适合存小数据:开关、主题、token、上次选择的月份。不要存列表。
- 读数据是异步的,不要在 build 里 await;放
initState或用 Riverpod 的 override 提前注入。 - 支持的类型:
String / int / double / bool / List<String>;对象要自己jsonEncode成字符串。 - 先
await setXxx成功,再setState,避免写盘失败但界面已变。
9.2 sqflite:本地数据库(列表数据、离线缓存)
创建步骤
flutter create sqflite_demo && cd sqflite_demo;flutter pub add sqflite path;- 替换
lib/main.dart,运行后新增几条,杀掉 App 重进验证数据仍在。
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
void main() => runApp(const MaterialApp(home: NotePage()));
class NotePage extends StatefulWidget {
const NotePage({super.key});
@override
State<NotePage> createState() => _NotePageState();
}
class _NotePageState extends State<NotePage> {
Database? _db;
List<Map<String, Object?>> _notes = [];
final _controller = TextEditingController();
@override
void initState() {
super.initState();
_initDb();
}
@override
void dispose() {
_controller.dispose();
_db?.close();
super.dispose();
}
Future<void> _initDb() async {
final dir = await getDatabasesPath();
_db = await openDatabase(
p.join(dir, 'notes.db'),
version: 1,
onCreate: (db, version) async {
// 建表只会在首次创建时执行一次
await db.execute('''
CREATE TABLE notes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
created_at INTEGER NOT NULL
)
''');
},
);
await _load();
}
Future<void> _load() async {
final rows = await _db!.query('notes', orderBy: 'id DESC');
if (!mounted) return;
setState(() => _notes = rows);
}
Future<void> _add() async {
final text = _controller.text.trim();
if (text.isEmpty) return;
await _db!.insert('notes', {
'content': text,
'created_at': DateTime.now().millisecondsSinceEpoch,
});
_controller.clear();
await _load();
}
Future<void> _delete(int id) async {
await _db!.delete('notes', where: 'id = ?', whereArgs: [id]);
await _load();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SQLite 便签')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: '写点什么…'),
onSubmitted: (_) => _add(),
),
),
const SizedBox(width: 8),
FilledButton(onPressed: _add, child: const Text('保存')),
],
),
),
Expanded(
child: _notes.isEmpty
? const Center(child: Text('还没有便签'))
: ListView.separated(
itemCount: _notes.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final note = _notes[index];
return ListTile(
title: Text(note['content'] as String),
subtitle: Text(
DateTime.fromMillisecondsSinceEpoch(
note['created_at'] as int)
.toString()
.substring(0, 16),
style: const TextStyle(fontSize: 11),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _delete(note['id'] as int),
),
);
},
),
),
],
),
);
}
}
关键点
getDatabasesPath()拿到的是应用私有目录,卸载 App 才会清除。- 建表必须在
onCreate里;后续加字段要提升version并在onUpgrade里写迁移。 - 路径拼接用
package:path的join,不要手拼/(Windows 与 Android 分隔符不同)。 - 所有写操作后重新查询再
setState,保证界面与数据库一致。 - sqflite 只支持 Android/iOS,桌面与 Web 需要另选 drift/isar 等方案。
动态效果 · 写入本地后重进仍在
💾
① 写入 SharedPreferences / SQLite
② 杀掉进程
③ 重新打开:数据仍在 ✓