Flutter 小白入门教程Flutter 小白入门教程
首页
学习指南
项目实战
Flutter 官网
编程指南
首页
学习指南
项目实战
Flutter 官网
编程指南
  • 入门基础

    • 📚 基础教程
    • 第1章 - 认识 Flutter
    • 第2章 - 环境搭建
    • 第3章 - Dart 语言基础
    • 第4章 - 第一个 Flutter 应用
    • 第5章 - Widget 基础
    • 第6章 - 布局系统
    • 第7章 - 状态管理入门
    • 第8章 - 页面导航
    • 第9章 - 资源管理
  • 进阶开发

    • 第10章 - 网络请求
    • 第11章 - 本地存储
    • 第12章 - 对话框与反馈
    • 第13章 - 列表进阶
    • 第14章 - 主题定制
    • 第15章 - 状态管理进阶
    • 第16章 - 动画入门
    • 第17章 - 常用第三方包
  • 调试与发布

    • 第18章 - 调试与性能优化
    • 第19章 - 打包与发布
  • 附录

    • 附录A - UI 框架与组件库推荐
    • 附录B - 项目结构最佳实践
    • 附录C - 国际化配置
    • 附录D - 权限处理

第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});

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  bool _darkMode = false;
  bool _notifications = true;
  String _language = '中文';

  @override
  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);
    }
  }

  @override
  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});

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

class _TodoListPageState extends State<TodoListPage> {
  List<Todo> _todos = [];
  final TextEditingController _controller = TextEditingController();

  @override
  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();
  }

  @override
  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 - 结构化数据库存储
  • ✅ 各种存储方案的封装和最佳实践

存储方案选择指南

数据类型推荐方案
用户设置、TokenSharedPreferences
简单的缓存数据JSON 文件
结构化数据、需要查询SQLite
大量非结构化数据文件存储

💪 练习题

  1. 使用 SharedPreferences 实现一个「记住密码」功能
  2. 使用文件存储实现一个简单的笔记本应用
  3. 使用 SQLite 实现一个完整的待办事项应用

🚀 下一步

学会了本地存储后,下一章我们来学习 对话框与反馈,提升用户体验!


由 编程指南 提供

最近更新: 2026/2/3 16:24
Contributors: 王长安
Prev
第10章 - 网络请求
Next
第12章 - 对话框与反馈