Flutter 入门第九课:本地存储实战(SharedPreferences + 文件 + SQLite)
这节课是 Flutter 实现数据本地持久化的核心也是 APP 开发的必备能力 —— 解决「重启后数据丢失」的问题实现登录状态保存、离线缓存、历史记录、本地配置等核心业务场景。我们会系统学习 Flutter 三大本地存储方案按轻量→中等→重量级划分适配不同业务需求SharedPreferences轻量键值对存储首选适配 80% 场景文件存储本地文件 / 图片存储适配大文件、自定义格式数据SQLite本地关系型数据库适配大量结构化数据如本地列表、离线数据库同时结合前几节课的Dio 网络请求和状态管理实现登录状态持久化「网络请求→本地缓存→全局共享→重启保留」的全流程闭环贴合企业真实开发场景。课前回顾网络请求Dio 全局配置、GET/POST、拦截器统一处理 token状态管理InheritedWidget 实现全局状态共享用户信息前置基础async/await异步操作、实体类序列化json_serializable核心需求本地存储的核心是持久化——APP 重启、手机关机后数据不丢失。一、本地存储方案选型指南Flutter 的本地存储方案均基于原生端实现Android/iOS 各自的存储方案Flutter 封装统一 API无需关心原生差异开发时按数据类型、数据量、访问效率选型即可以下是企业开发的标准选型原则表格存储方案底层实现数据格式适用场景数据量核心优势核心劣势SharedPreferencesAndroidSP / iOSNSUserDefaults键值对String/int/bool/double/List登录 token、用户信息、APP 配置、开关状态、轻量缓存小KB 级用法简单、API 友好、跨平台统一不支持复杂对象、数据量大会卡顿文件存储Android/iOS本地文件系统沙盒自定义文本 / JSON / 二进制 / 图片大文件、图片 / 视频缓存、自定义格式数据、日志文件中MB/GB 级支持任意格式、存储无上限受手机内存限制需手动管理文件、解析数据、处理文件路径SQLiteAndroid/iOSSQLite 数据库关系型表结构结构化数据本地列表、离线数据库、历史记录、大量结构化数据大GB 级支持 SQL 查询、事务、索引访问效率高用法复杂、需建表 / 写 SQL、学习成本高核心选型原则优先使用 SharedPreferences80% 的本地存储需求如 token、用户信息、配置项都能满足用法最简单开发效率最高大文件 / 自定义格式用文件存储如图片缓存、PDF/Excel 文件、自定义日志文件大量结构化数据用 SQLite如本地商品列表、离线聊天记录、需要分页 / 条件查询的海量数据禁止用 SharedPreferences 存大量 / 复杂数据如整个列表、嵌套对象会导致 APP 启动卡顿、存储失败。二、SharedPreferences轻量键值对存储开发首选SharedPreferences简称 SP是 Flutter最常用、最基础的本地存储方案适配所有轻量键值对存储场景官方推荐使用第三方库shared_preferencesFlutter 团队维护稳定无坑而非原生 API。步骤 1集成依赖添加最新稳定版依赖到pubspec.yaml执行flutter pub get安装yamldependencies: flutter: sdk: flutter shared_preferences: ^2.2.2 # SP核心依赖步骤 2封装 SP 工具类企业级规范禁止在页面中直接使用 SPAPI会导致代码重复、管理混乱企业开发中会封装全局 SP 工具类提供统一的增删改查方法隐藏底层实现便于后续维护和替换。创建lib/utils/sp_utils.dart封装通用方法支持基本类型和JSON 对象如用户信息实体类的存储 / 读取dartimport package:shared_preferences/shared_preferences.dart; /// SharedPreferences 工具类单例 class SPUtils { // 单例实例保证全局唯一 static late SharedPreferences _instance; // 初始化SP在APP启动时执行main函数中 static Futurevoid init() async { _instance await SharedPreferences.getInstance(); } // ---------------------- 基本类型操作 ---------------------- static Futurebool setString(String key, String value) _instance.setString(key, value); static Futurebool setInt(String key, int value) _instance.setInt(key, value); static Futurebool setBool(String key, bool value) _instance.setBool(key, value); static Futurebool setDouble(String key, double value) _instance.setDouble(key, value); static Futurebool setStringList(String key, ListString value) _instance.setStringList(key, value); static String getString(String key, {String defValue }) _instance.getString(key) ?? defValue; static int getInt(String key, {int defValue 0}) _instance.getInt(key) ?? defValue; static bool getBool(String key, {bool defValue false}) _instance.getBool(key) ?? defValue; static double getDouble(String key, {double defValue 0.0}) _instance.getDouble(key) ?? defValue; static ListString getStringList(String key, {ListString defValue const []}) _instance.getStringList(key) ?? defValue; // ---------------------- 自定义对象操作JSON ---------------------- // 存储对象将实体类转为JSON字符串存储 static Futurebool setObject(String key, Object obj) { String jsonStr obj is String ? obj : _encodeObjToJson(obj); return setString(key, jsonStr); } // 读取对象将JSON字符串转为指定类型实体类 static T? getObjectT(String key, T Function(MapString, dynamic) fromJson) { String jsonStr getString(key); if (jsonStr.isEmpty) return null; return fromJson(_decodeJsonToMap(jsonStr)); } // ---------------------- 通用操作 ---------------------- // 删除指定key static Futurebool remove(String key) _instance.remove(key); // 清空所有SP数据 static Futurebool clear() _instance.clear(); // 判断key是否存在 static bool containsKey(String key) _instance.containsKey(key); // ---------------------- 私有工具方法 ---------------------- // 对象转JSON字符串 static String _encodeObjToJson(Object obj) { if (obj is Map || obj is List) { return const JsonEncoder().convert(obj); } throw Exception(仅支持Map/List/实体类需实现toJson转JSON); } // JSON字符串转Map static MapString, dynamic _decodeJsonToMap(String jsonStr) { return const JsonDecoder().convert(jsonStr); } }封装亮点单例模式全局唯一 SP 实例避免多次初始化基础类型全覆盖提供 String/int/bool 等所有基础类型的增删改查支持实体类存储通过JSON 序列化实现复杂对象如 UserBean的存储解决 SP 不支持复杂对象的问题统一初始化需在 main 函数中初始化保证使用前 SP 已就绪隐藏底层细节页面只需调用SPUtils.setString/setObject无需关心 SP 的底层实现。步骤 3在 main 函数中初始化 SP修改main.dart在 APP 启动时初始化Dio和SP保证全局工具类就绪dartimport package:flutter/material.dart; import package:xxx/utils/network_utils.dart; import package:xxx/utils/sp_utils.dart; import app_root.dart; // 全局状态根组件 // 异步main函数支持初始化异步工具类 void main() async { // 必须添加确保Flutter绑定初始化完成异步main必备 WidgetsFlutterBinding.ensureInitialized(); // 初始化Dio和SP await initDio(); await SPUtils.init(); // 运行APP runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); override Widget build(BuildContext context) { return const MaterialApp( title: Flutter本地存储实战, theme: ThemeData(primarySwatch: Colors.blue), debugShowCheckedModeBanner: false, home: AppRoot(), // 包裹InheritedWidget的根组件 ); } }关键异步 main 函数必须添加WidgetsFlutterBinding.ensureInitialized();否则会报「Flutter 绑定未初始化」错误。步骤 4SP 核心实战 —— 登录状态持久化核心业务结合SP 存储、Dio 拦截器、InheritedWidget 全局状态实现企业级登录状态持久化核心流程登录页调用登录接口获取 token 和用户信息本地缓存将 token 和用户信息实体类存入 SP全局共享更新 InheritedWidget 的全局用户状态请求拦截Dio 请求拦截器自动从 SP 读取 token添加到请求头APP 重启启动时从 SP 读取用户信息初始化全局状态实现「重启保留登录状态」。实战 1定义用户实体类支持 JSON 序列化确保UserBean实现toJson/fromJsonjson_serializable 自动生成用于 SP 的对象存储dart// lib/model/user_bean.dart import package:json_annotation/json_annotation.dart; part user_bean.g.dart; JsonSerializable() class UserBean { final String token; // 登录token final String phone; // 手机号 final String name; // 用户名 final bool isLogin; // 登录状态 UserBean({ this.token , this.phone , this.name 游客, this.isLogin false, }); // 从JSON解析 factory UserBean.fromJson(MapString, dynamic json) _$UserBeanFromJson(json); // 转为JSON MapString, dynamic toJson() _$UserBeanToJson(this); // 复制方法修改状态 UserBean copyWith({ String? token, String? phone, String? name, bool? isLogin, }) { return UserBean( token: token ?? this.token, phone: phone ?? this.phone, name: name ?? this.name, isLogin: isLogin ?? this.isLogin, ); } }实战 2登录页 —— 登录成功后缓存用户信息登录页调用登录接口成功后通过SPUtils.setObject存储用户实体类同时更新全局状态dart// lib/pages/login_page.dart import package:flutter/material.dart; import package:xxx/model/user_bean.dart; import package:xxx/utils/sp_utils.dart; import package:xxx/inherited/user_inherited_widget.dart; import package:xxx/utils/network_utils.dart; class LoginPage extends StatefulWidget { const LoginPage({super.key}); override StateLoginPage createState() _LoginPageState(); } class _LoginPageState extends StateLoginPage { final TextEditingController _phoneController TextEditingController(); final TextEditingController _pwdController TextEditingController(); // 模拟登录接口 FutureUserBean _login(String phone, String pwd) async { // 实际开发中替换为真实POST登录接口 await Future.delayed(const Duration(seconds: 1)); // 模拟返回用户信息和token return UserBean( token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., // 模拟token phone: phone, name: Flutter开发者, isLogin: true, ); } // 登录按钮点击事件 void _onLogin() async { String phone _phoneController.text.trim(); String pwd _pwdController.text.trim(); if (phone.isEmpty || pwd.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text(请输入手机号和密码))); return; } try { // 1. 调用登录接口获取用户信息 UserBean user await _login(phone, pwd); // 2. 本地缓存将用户信息存入SP核心持久化 await SPUtils.setObject(user_info, user); // 3. 更新全局状态通知所有子组件刷新 UserInheritedWidget.of(context).updateUser(user); // 4. 跳转到首页 Navigator.pushReplacementNamed(context, /home); } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(登录失败$e))); } } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(登录)), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 手机号输入框使用封装的通用组件 CommonTextField( controller: _phoneController, hintText: 请输入手机号, prefixIcon: Icons.phone, keyboardType: TextInputType.phone, ), const SizedBox(height: 20), // 密码输入框 CommonTextField( controller: _pwdController, hintText: 请输入密码, prefixIcon: Icons.lock, obscureText: true, ), const SizedBox(height: 40), // 登录按钮 CommonElevatedButton(text: 登录, onPressed: _onLogin), ], ), ), ); } }实战 3APP 启动时 —— 从 SP 加载用户信息初始化全局状态修改全局状态根组件AppRoot在initState中从 SP 读取用户信息实现重启保留登录状态dart// lib/app_root.dart import package:flutter/material.dart; import package:xxx/model/user_bean.dart; import package:xxx/utils/sp_utils.dart; import package:xxx/inherited/user_inherited_widget.dart; import package:xxx/pages/login_page.dart; import package:xxx/pages/home_page.dart; class AppRoot extends StatefulWidget { const AppRoot({super.key}); override StateAppRoot createState() _AppRootState(); } class _AppRootState extends StateAppRoot { late UserBean _user; override void initState() { super.initState(); // 初始化从SP读取用户信息实现登录状态持久化 _initUserFromSP(); } // 从SP加载用户信息 void _initUserFromSP() { // 从SP读取用户实体类 UserBean? user SPUtils.getObjectUserBean(user_info, UserBean.fromJson); // 若未登录使用默认游客状态 _user user ?? UserBean(); } // 更新用户状态 void _updateUser(UserBean newUser) { setState(() { _user newUser; }); } // 判断是否登录跳转到对应页面 Widget _getInitPage() { return _user.isLogin ? const HomePage() : const LoginPage(); } override Widget build(BuildContext context) { return UserInheritedWidget( user: _user, updateUser: _updateUser, child: _getInitPage(), // 根据登录状态渲染初始页面 ); } }实战 4Dio 拦截器 —— 从 SP 自动读取 token添加到请求头修改network_utils.dart的 Dio 请求拦截器从 SP 读取用户信息中的 token实现所有请求自动携带 token无需在每个请求中单独写dart// lib/utils/network_utils.dart import package:dio/dio.dart; import package:xxx/model/user_bean.dart; import package:xxx/utils/sp_utils.dart; final Dio dio Dio(); void initDio() { dio.options.baseUrl https://xxx.com/; dio.options.connectTimeout const Duration(seconds: 5); dio.options.receiveTimeout const Duration(seconds: 5); dio.options.headers {Content-Type: application/json;charsetutf-8}; // 添加拦截器 dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { // 从SP读取用户信息获取token UserBean? user SPUtils.getObjectUserBean(user_info, UserBean.fromJson); if (user ! null user.isLogin user.token.isNotEmpty) { // 自动添加token到请求头Bearer认证 options.headers[Authorization] Bearer ${user.token}; } handler.next(options); }, onError: (e, handler) { // 统一处理错误 String errorMsg _handleDioError(e); print(全局网络错误$errorMsg); handler.reject(DioException(requestOptions: e.requestOptions, message: errorMsg)); }, )); // 日志拦截器 dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true)); } String _handleDioError(DioException e) { switch (e.type) { case DioExceptionType.connectionTimeout: return 网络连接超时; case DioExceptionType.connectionError: return 网络连接错误; case DioExceptionType.badResponse: return 接口错误${e.response?.statusCode}; default: return e.message ?? 未知网络错误; } }实战 5退出登录 —— 清除 SP 数据 重置全局状态在首页 / 我的页面实现退出登录功能清除 SP 中的用户信息重置全局状态为游客跳转到登录页dart// 退出登录方法 void _onLogout() async { // 1. 清除SP中的用户信息 await SPUtils.remove(user_info); // 2. 重置全局用户状态为游客 UserInheritedWidget.of(context).updateUser(UserBean()); // 3. 跳转到登录页禁止返回 Navigator.pushReplacementNamed(context, /login); }SP 核心总结封装是必选项全局 SP 工具类是企业开发的标准避免代码重复便于维护复杂对象通过 JSON 存储SP 本身不支持复杂对象通过「实体类→JSON 字符串→SP 存储」实现登录状态持久化核心APP启动从SP加载→登录时SP存储更新全局状态→退出时SP清除重置状态禁止存大量数据SP 适合轻量数据大量数据会导致 APP 启动卡顿建议用 SQLite 替代。三、文件存储本地文件 / 图片存储文件存储适用于大文件、自定义格式数据Flutter 通过path_provider库获取本地沙盒路径避免文件权限问题结合 Dart 原生的io库实现文件的创建、读取、写入、删除支持文本文件、JSON 文件、二进制文件图片 / 视频。核心概念Flutter 本地沙盒路径Flutter 的文件存储基于原生沙盒每个 APP 有独立的沙盒目录其他 APP 无法访问保证数据安全path_provider库提供 3 个核心目录开发时按需选择应用文档目录getApplicationDocumentsDirectory持久化存储APP 卸载前不会被删除适合存储用户数据、重要文件临时目录getTemporaryDirectory临时存储系统会自动清理如低内存时适合存储缓存、临时文件外部存储目录getExternalStorageDirectory仅 Android 支持适合存储大文件、图片 / 视频iOS 无此概念。步骤 1集成依赖需要两个核心依赖path_provider获取沙盒路径、dart:ioDart 原生文件操作无需集成yamldependencies: flutter: sdk: flutter path_provider: ^2.1.1 # 获取本地路径步骤 2封装文件存储工具类创建lib/utils/file_utils.dart封装路径获取、文本文件操作、图片文件操作的通用方法隐藏底层路径和 IO 操作dartimport dart:io; import dart:typed_data; import package:path_provider/path_provider.dart; import package:path/path.dart as path; /// 文件存储工具类 class FileUtils { // ---------------------- 路径获取 ---------------------- // 获取应用文档目录持久化推荐 static FutureDirectory getDocDir() async await getApplicationDocumentsDirectory(); // 获取临时目录临时缓存 static FutureDirectory getTempDir() async await getTemporaryDirectory(); // 拼接文件完整路径目录文件名 static FutureString getFilePath(String fileName, {bool isTemp false}) async { Directory dir isTemp ? await getTempDir() : await getDocDir(); return path.join(dir.path, fileName); } // ---------------------- 文本/JSON文件操作 ---------------------- // 写入文本文件支持JSON字符串 static FutureFile writeTextFile(String fileName, String content, {bool isTemp false}) async { String filePath await getFilePath(fileName, isTemp: isTemp); File file File(filePath); return await file.writeAsString(content); } // 读取文本文件 static FutureString readTextFile(String fileName, {bool isTemp false}) async { String filePath await getFilePath(fileName, isTemp: isTemp); File file File(filePath); if (await file.exists()) { return await file.readAsString(); } throw Exception(文件不存在$fileName); } // ---------------------- 二进制文件操作图片/视频 ---------------------- // 写入二进制文件如图片的Uint8List static FutureFile writeBytesFile(String fileName, Uint8List bytes, {bool isTemp false}) async { String filePath await getFilePath(fileName, isTemp: isTemp); File file File(filePath); return await file.writeAsBytes(bytes); } // 读取二进制文件 static FutureUint8List readBytesFile(String fileName, {bool isTemp false}) async { String filePath await getFilePath(fileName, isTemp: isTemp); File file File(filePath); if (await file.exists()) { return await file.readAsBytes(); } throw Exception(文件不存在$fileName); } // ---------------------- 通用文件操作 ---------------------- // 判断文件是否存在 static Futurebool fileExists(String fileName, {bool isTemp false}) async { String filePath await getFilePath(fileName, isTemp: isTemp); return await File(filePath).exists(); } // 删除文件 static Futurebool deleteFile(String fileName, {bool isTemp false}) async { String filePath await getFilePath(fileName, isTemp: isTemp); File file File(filePath); if (await file.exists()) { await file.delete(); return true; } return false; } }实战图片网络缓存网络图片→本地文件→本地读取结合 Dio 和文件存储实现图片网络缓存—— 首次加载从网络获取保存到本地临时目录后续加载直接从本地读取提升加载速度减少网络请求dart// 图片缓存方法优先从本地读取本地无则从网络下载并缓存 FutureFile getImageCache(String imageUrl) async { // 1. 将图片URL转为唯一文件名避免重复 String fileName imageUrl.split(/).last; // 2. 判断本地是否有缓存 if (await FileUtils.fileExists(fileName, isTemp: true)) { String filePath await FileUtils.getFilePath(fileName, isTemp: true); return File(filePath); } // 3. 本地无缓存从网络下载图片Dio获取二进制数据 Response response await dio.get( imageUrl, responseType: ResponseType.bytes, // 以二进制形式获取 ); // 4. 将二进制数据写入本地临时文件 Uint8List bytes response.data; await FileUtils.writeBytesFile(fileName, bytes, isTemp: true); // 5. 返回本地文件 String filePath await FileUtils.getFilePath(fileName, isTemp: true); return File(filePath); } // 用法在Image组件中使用 // File imageFile await getImageCache(https://picsum.photos/200/200); // Image.file(imageFile, fit: BoxFit.cover);四、SQLite本地关系型数据库大量结构化数据SQLite 是轻量型关系型数据库无需服务端直接运行在本地支持 SQL 语句、事务、索引适合存储大量结构化数据如本地商品列表、离线聊天记录、历史搜索记录。Flutter 中最主流的 SQLite 库是sqfliteFlutter 团队维护结合path_provider获取数据库路径实现跨平台数据库操作。核心概念数据库一个 APP 可创建多个数据库后缀为.db表数据库的基本单位按关系型结构存储数据行 记录列 字段SQL 语句实现表的创建、数据的增删改查CRUD事务保证多个操作的原子性要么全部成功要么全部失败实体类映射表的字段与实体类的属性一一对应。步骤 1集成依赖需要两个核心依赖sqfliteSQLite 操作、path_provider获取数据库路径yamldependencies: flutter: sdk: flutter sqflite: ^2.3.2 # SQLite核心 path_provider: ^2.1.1 # 获取数据库路径 path: ^1.8.3 # 路径拼接步骤 2封装数据库工具类 创建表创建lib/utils/db_utils.dart封装数据库初始化、表创建、通用 CRUD 方法以「历史搜索记录」为例创建search_history表实现增删改查dartimport dart:io; import package:sqflite/sqflite.dart; import package:path_provider/path_provider.dart; import package:path/path.dart as path; /// 数据库工具类单例 class DBUtils { // 单例实例 static late Database _db; // 数据库名称 static const String _dbName flutter_db.db; // 数据库版本升级时需修改 static const int _dbVersion 1; // 历史搜索记录表 static const String tableSearchHistory search_history; // 初始化数据库创建数据库表 static Futurevoid init() async { // 1. 获取数据库路径 Directory docDir await getApplicationDocumentsDirectory(); String dbPath path.join(docDir.path, _dbName); // 2. 打开/创建数据库 _db await openDatabase( dbPath, version: _dbVersion, onCreate: (db, version) async { // 3. 创建表历史搜索记录表id:主键自增content:搜索内容time:搜索时间 await db.execute( CREATE TABLE $tableSearchHistory ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, time INTEGER NOT NULL ) ); }, onUpgrade: (db, oldVersion, newVersion) { // 数据库升级时执行如新增字段、表 }, ); } // ---------------------- 通用CRUD方法 ---------------------- // 插入数据 static Futureint insert(String table, MapString, dynamic data) async { return await _db.insert(table, data, conflictAlgorithm: ConflictAlgorithm.replace); } // 查询数据条件/排序/分页 static FutureListMapString, dynamic query( String table, { ListString? columns, String? where, Listdynamic? whereArgs, String? orderBy, int? limit, int? offset, }) async { return await _db.query( table, columns: columns, where: where, whereArgs: whereArgs, orderBy: orderBy, limit: limit, offset: offset, ); } // 更新数据 static Futureint update( String table, MapString, dynamic data, { String? where, Listdynamic? whereArgs, }) async { return await _db.update(table, data, where: where, whereArgs: whereArgs); } // 删除数据 static Futureint delete( String table, { String? where, Listdynamic? whereArgs, }) async { return await _db.delete(table, where: where, whereArgs: whereArgs); } // 执行原生SQL static Futurevoid execute(String sql, [Listdynamic? arguments]) async { await _db.execute(sql, arguments); } // 开启事务 static FutureT transactionT(FutureT Function(Transaction txn) action) async { return await _db.transaction(action); } // 关闭数据库 static Futurevoid close() async await _db.close(); // ---------------------- 历史搜索记录专属方法 ---------------------- // 插入搜索记录 static Futureint insertSearchHistory(String content) async { MapString, dynamic data { content: content, time: DateTime.now().millisecondsSinceEpoch, }; return await insert(tableSearchHistory, data); } // 查询所有搜索记录按时间倒序 static FutureListMapString, dynamic getSearchHistory() async { return await query( tableSearchHistory, orderBy: time DESC, ); } // 删除单条搜索记录 static Futureint deleteSearchHistory(int id) async { return await delete( tableSearchHistory, where: id ?, whereArgs: [id], ); } // 清空所有搜索记录 static Futureint clearSearchHistory() async { return await delete(tableSearchHistory); } }步骤 3初始化数据库在main.dart中添加数据库初始化与 Dio、SP 一起初始化dartvoid main() async { WidgetsFlutterBinding.ensureInitialized(); await initDio(); await SPUtils.init(); await DBUtils.init(); // 初始化SQLite runApp(const MyApp()); }步骤 4使用数据库 —— 历史搜索记录实战在搜索页实现添加搜索记录、查询搜索记录、删除搜索记录的功能直接调用 DBUtils 的专属方法无需写 SQL 语句dart// 插入搜索记录 await DBUtils.insertSearchHistory(Flutter本地存储); // 查询所有搜索记录 ListMapString, dynamic historyList await DBUtils.getSearchHistory(); // 转为实体类列表可选 ListSearchHistoryModel historyModelList historyList.map((e) SearchHistoryModel.fromJson(e)).toList(); // 删除单条搜索记录 await DBUtils.deleteSearchHistory(1); // 清空所有搜索记录 await DBUtils.clearSearchHistory();五、本节课核心总结必背本地存储全考点1. 存储方案选型核心80% 场景用 SharedPreferences轻量键值对、登录状态、配置项首选封装后的 SP 工具类大文件 / 图片用文件存储通过 path_provider 获取沙盒路径io 库实现文件操作核心是「路径拼接 格式统一」大量结构化数据用 SQLite需建表 / 写 SQL适合本地列表、离线数据库支持事务和条件查询2. SharedPreferences 核心登录状态持久化核心流程APP启动SP加载→登录SP存储全局更新→退出SP清除全局重置复杂对象存储通过 JSON 序列化实体类→JSON 字符串必须封装全局单例工具类避免代码重复和管理混乱3. 文件存储核心沙盒路径优先使用应用文档目录持久化和临时目录缓存核心操作路径获取→文件写入→文件读取→文件删除图片缓存网络二进制数据→本地文件存储→后续本地读取4. SQLite 核心核心流程数据库初始化→创建表→CRUD操作封装原则通用 CRUD 方法 业务专属方法隐藏 SQL 语句关键特性事务保证原子性按时间 / 条件排序查询5. 企业开发最佳实践统一初始化Dio、SP、SQLite 在 main 函数中异步初始化添加WidgetsFlutterBinding.ensureInitialized()分层封装所有本地存储工具类放在lib/utils/页面只调用方法不直接操作底层 API实体类映射SP / 文件 / SQLite 的存储数据与实体类一一对应禁止直接使用 Map异常处理所有本地存储操作都是异步的必须添加try/catch捕获异常资源释放文件 / 数据库使用完成后及时关闭避免资源泄漏六、课后练习本地存储必备贴合企业场景基础练习基于 SP 实现APP 夜间模式持久化—— 切换夜间模式时存储到 SPAPP 重启后保持夜间模式状态进阶练习基于文件存储实现用户头像本地缓存—— 从网络下载头像图片保存到应用文档目录修改头像时更新本地文件头像组件优先读取本地文件实战练习基于 SQLite 实现本地商品列表—— 将网络请求的商品列表存入 SQLite实现离线查看商品下拉刷新时同步网络数据更新本地数据库。下一节课预告我们会学习 Flutter 的路由与导航进阶企业级解决基础路由的痛点命名路由统一管理路由表实现无耦合的页面跳转路由传参基础类型 / 实体类 / 回调函数传参解决复杂传参问题路由拦截实现登录拦截未登录时跳转到登录页、权限拦截页面转场动画自定义页面跳转 / 返回的动画提升用户体验路由管理获取当前路由、返回上一页、返回到根页面、关闭所有页面路由是 APP 页面跳转的核心企业开发中必须使用命名路由 路由拦截实现页面的解耦和权限控制结合之前的登录状态持久化实现完整的权限控制体系我可以帮你把本节课的SP 工具类、文件工具类、数据库工具类整合为一个可直接复用的 Flutter 工具库包含登录状态持久化、图片缓存、历史记录的完整代码需要吗