第 9 章 数据持久化(两个完整示例)

9.1 shared_preferences:记住一个值(设置项、token)

创建步骤
  1. flutter create prefs_demo && cd prefs_demo;
  2. flutter pub add shared_preferences;
  3. 替换 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(),
      ),
    );
  }
}
关键点

9.2 sqflite:本地数据库(列表数据、离线缓存)

创建步骤
  1. flutter create sqflite_demo && cd sqflite_demo;
  2. flutter pub add sqflite path;
  3. 替换 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),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}
关键点
动态效果 · 写入本地后重进仍在
💾
① 写入 SharedPreferences / SQLite
② 杀掉进程
③ 重新打开:数据仍在 ✓
← 上一章第 8 章 网络请求与 JSON 解析(完整可运行示例)下一章 →第 10 章 动画(完整可运行示例)
✉ 联系作者:linghurongjun@sina.com