当前位置: 首页 > article >正文

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 工具库包含登录状态持久化、图片缓存、历史记录的完整代码需要吗

相关文章:

Flutter 入门第九课:本地存储实战(SharedPreferences + 文件 + SQLite)

这节课是 Flutter 实现数据本地持久化的核心,也是 APP 开发的必备能力 —— 解决「重启后数据丢失」的问题,实现登录状态保存、离线缓存、历史记录、本地配置等核心业务场景。我们会系统学习 Flutter 三大本地存储方案,按轻量→中等→重量级划…...

静息态fMRI预处理实战:从DICOM到ALFF的完整流程解析

1. 静息态fMRI预处理入门指南 第一次接触静息态fMRI数据时,我被满屏的DICOM文件搞得晕头转向。这些医学影像数据就像一堆未经整理的拼图碎片,需要我们通过预处理流程将它们转化为可分析的标准化数据。静息态fMRI(rs-fMRI)记录了大…...

Unity shader中TransformWorldToShadowCoord原理解析

TransformWorldToShadowCoord 的核心作用很简单:将你提供的世界坐标,转换到一个可以用于采样 Shadow Map 的坐标空间。它本质上是为你省去了手动编写矩阵乘法的繁琐步骤。🔍 核心原理:一个“三步走”的幕后过程函数内部主要执行了…...

从试点到全栈替代:SITS2026中台团队用AI编程工具重构127个微服务的完整迁移路线图(含Git提交行为分析数据)

第一章:SITS2026案例:大厂AI编程工具实践 2026奇点智能技术大会(https://ml-summit.org) 在2026奇点智能技术大会(SITS2026)的工业实践分论坛中,某头部云厂商首次完整公开其内部AI编程助手“CodePilot Pro”的落地路…...

Unity ApplyShadowBias 返回什么,什么是Shadow Map 采样,什么是阴影 acne(纹波/摩尔纹) 和 peter-panning(悬空阴影)

在 Unity 的阴影渲染中,这几个概念紧密相关,理解它们能帮你更好地调试阴影效果。1. ApplyShadowBias 返回什么?ApplyShadowBias 是 Unity 内部用于渲染阴影贴图(Shadow Map)时的一个函数,你通常不会直接调用…...

揭秘OpenAI、DeepMind未公开的XAGI白皮书核心章节:4类不可协商的透明度基线要求

第一章:AGI的决策透明度与可解释性 2026奇点智能技术大会(https://ml-summit.org) AGI系统在医疗诊断、司法辅助与金融风控等高敏场景中的部署,正迫使研究者重新审视“黑箱”决策的伦理边界。当模型输出直接影响生命权、自由权或财产权时,仅…...

Linux内核中的设备驱动详解

Linux内核中的设备驱动详解 引言 设备驱动是Linux内核中的重要组成部分,它负责管理硬件设备与内核之间的通信。设备驱动为应用程序提供了访问硬件设备的接口,使得应用程序可以无需了解硬件的具体实现细节。本文将深入探讨Linux内核中的设备驱动机制&…...

Spring MVC 01

什么是Spring Web MVC Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从⼀开始就包含在 Spring 框架中。它的正式名称“Spring Web MVC”来⾃其源模块的名称(Spring-webmvc),但它通常被称为"Spring MVC" 然⽽要真正的理解什么是…...

AI 新闻周报 | 2026年4月12日-4月18日

AI 新闻周报 | 2026年4月12日-4月18日 📅 周期:2026年4月12日 - 4月18日 📝 一句话总结:大模型巨头密集发布旗舰产品,AI 安全与能力的博弈白热化;具身智能融资狂飙、工业落地加速;全球 AI 监管框…...

FairyGUI按钮动效实战:从点击缩放+音效到复杂转场,一个完整项目案例拆解

FairyGUI按钮动效实战:从点击反馈到复杂转场的全流程解决方案 在游戏界面开发中,按钮动效不仅仅是装饰,更是用户体验的关键组成部分。一个精心设计的按钮动效能够显著提升用户的操作反馈感,而流畅的界面转场则能增强应用的沉浸感…...

Hailo8 Dataflow Compiler 模型转换指南--以 ONNX 模型为例

目录 一、环境安装 1.1 系统要求 1.2 系统包安装 二、模型转换 2.1 ONNX 转 HEF 文件 2.1.1 实例化 ClientRunner 解析ONNX模型 2.2.2 加载/准备量化校准数据集 2.2.3 参数配置并执行量化操作 2.2.4 保存 HAR 文件并编译生成 HEF 板端文件 2.2 模型可视化 一、环境安…...

用python解放右手系列(三) Excel自动化-告别复制粘贴的噩梦

Excel 自动化:告别复制粘贴的噩梦本文基于 Python 3.9,涉及库:pandas、openpyxl。阅读时间约 12 分钟。 安装依赖:pip install pandas openpyxl每月 1 号的"酷刑" 阿明刚用 Python 搞定文件重命名,还没高兴两…...

MusePublic Art Studio生成多样性控制:潜在空间探索技术

MusePublic Art Studio生成多样性控制:潜在空间探索技术 说实话,用AI生成艺术图片,最让人头疼的可能不是“画不出来”,而是“画得都一样”。你输入一段描述,比如“一个赛博朋克风格的武士”,模型确实能给你…...

COMSOL 超表面仿真:从入门到“光速”出图!

在系统讲解天线、超表面的物理原理、功能实现机制以及利用有限元法(Finite Element Method, FEM)进行建模与仿真设计的完整流程。通过理论讲授与仿真实践相结合的方式,帮助学员掌握从结构建模、物理场设置、网格划分、参数扫描到仿真后处理与…...

低分辨率图像修复难题的终极解决方案:Upscayl深度技术解析

低分辨率图像修复难题的终极解决方案:Upscayl深度技术解析 【免费下载链接】upscayl 🆙 Upscayl - #1 Free and Open Source AI Image Upscaler for Linux, MacOS and Windows. 项目地址: https://gitcode.com/GitHub_Trending/up/upscayl 面对模…...

《Hermes Agent 代码库安全漏洞分析与解决办法》

Hermes Agent 代码库安全漏洞分析与解决办法 Hermes Agent 作为跨平台自改进型 AI 智能体框架,涉及配置管理、多端通信、工具调用、容器部署等核心环节,以下从配置安全、部署安全、代码执行风险、数据隐私、网络通信、依赖管理、权限控制七大维度&#x…...

计算机毕业设计:Python农产品电商数据采集与价格预估平台 Flask框架 Spark 线性回归 数据分析 可视化 大数据 大模型(建议收藏)✅

1、项目介绍 技术栈 采用 Python 语言开发,基于 Flask 框架搭建后端服务,使用 Spark 技术进行大数据处理,通过 requests 爬虫从惠农网采集农产品数据,运用线性回归预测算法模型进行价格预测,前端结合 Echarts 可视化库…...

Intv_AI_MK11 Node.js 环境集成教程:构建全栈智能应用

Intv_AI_MK11 Node.js 环境集成教程:构建全栈智能应用 1. 开篇:为什么选择Node.js集成AI能力 如果你是一名Node.js开发者,想要给自己的应用添加AI能力,这篇教程就是为你准备的。我们将一步步带你完成从零开始的环境搭建&#xf…...

三分钟快速定位:Windows热键冲突终极解决方案指南

三分钟快速定位:Windows热键冲突终极解决方案指南 【免费下载链接】hotkey-detective A small program for investigating stolen key combinations under Windows 7 and later. 项目地址: https://gitcode.com/gh_mirrors/ho/hotkey-detective 你是否曾经按…...

从单兵到军团:2026 多智能体协作的崛起与实战全指南

从单兵到军团:2026 多智能体协作的崛起与实战全指南在前三篇文章中,我们拆解了单个AI Agent的技术内核,并盘点了2026年主流框架的选型策略。但企业级场景的复杂程度,正在以肉眼可见的速度超越单一个体的能力天花板。单个“全能实习…...

Cursor AI免费VIP破解方案:如何绕过试用限制持续使用Pro功能

Cursor AI免费VIP破解方案:如何绕过试用限制持续使用Pro功能 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pro 功能: Youve reached yo…...

PD协议中的VDM:从握手到模式切换的实战解析

1. VDM基础:从USB PD到厂商自定义消息 第一次接触USB PD协议中的VDM(Vendor Defined Message)时,我完全被各种缩写搞晕了。后来在实际项目中调试一个支持DisplayPort Alt Mode的扩展坞才发现,VDM简直是USB-C设备的&qu…...

在 Linux 中查询最耗费 CPU 资源的前 10 个进程的常用脚本

方法一:使用 ps 命令(推荐,最通用)#!/bin/bash # 查看CPU占用最高的10个进程 ps aux --sort-%cpu | head -n 11 | tail -n 10或者更详细的版本:#!/bin/bash echo " CPU使用率最高的10个进程 " printf "…...

051.数据库选型:为检测结果存储选择合适的数据库(SQLite/MySQL/PostgreSQL)

从一次线上事故说起 上个月深夜接到报警,部署在厂区的YOLO检测服务突然响应缓慢。登录服务器一看,发现检测结果写入数据库的线程全部卡死,前端页面加载历史记录要十几秒。查日志定位到问题:随着检测图片数量突破百万级,当初为了省事直接用的SQLite文件膨胀到8GB,并发写入…...

零代码经验也能搞定的软著申请:用AI工具30分钟生成合规材料

零代码经验也能搞定的软著申请:用AI工具30分钟生成合规材料 在数字化浪潮席卷各行各业的今天,软件著作权(简称"软著")已成为保护创新成果的重要法律凭证。无论是初创企业的核心产品、高校科研项目的技术输出&#xff0c…...

真的绝了!这套私域运营思路和方法让我效率提升10倍

你有没有发现,很多人做私域,每天花4.5小时在重复劳动上——回消息2小时、写朋友圈1小时、手动拉群0.5小时、跟进客户1小时。一年下来1642.5个小时,折合68天。结果呢?好友从3000删到800,月成交从50单跌到8单&#xff0c…...

Shopee卖家必看:如何用爬虫自动监控竞品评价与价格(Python实战)

Shopee卖家必看:如何用Python爬虫实现竞品评价与价格智能监控 在东南亚电商市场激烈竞争的今天,Shopee卖家们面临着一个共同的挑战:如何快速响应市场变化,及时调整运营策略?传统的人工监控方式不仅效率低下&#xff0…...

Flutter 三方库 serial 的鸿蒙化适配指南—如何在在鸿蒙系统上构建极致、稳定的 Web 串口通信与工业硬软连接实战

在工业数字化、设备物联网化、产线可视化运维的场景里,串口通信依然是最基础、最稳定、最具成本优势的设备连接方式之一。无论是 PLC、仪器仪表、扫码枪、称重模块,还是自定义 MCU 控制板,大量设备仍通过 UART/USB-Serial 与上位系统交换数据…...

C++数据成员指针

class Data1 { public:int a;char b; };int Data1:: * aa &Data1::a; 这行代码定义了一个指向 Data1 类中 int 类型成员变量的指针 aa,并将其初始化为指向成员 a。 int Data1::* 是指向 Data1 类中 int 类型数据成员的指针类型。aa 是指针变量的名字。&Dat…...

OpenGL渲染与几何内核那点事-项目实践理论补充(一-3-(6):从“搬砖”到“无人仓”:一个CAD极客的OpenGL性能压榨史,连AI都看呆了——给图形学新手的VBO/VAO全攻略)

TOC 代码仓库入口: github源码地址。gitee源码地址。 系列文章规划: OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(8)-番外篇:当你的 CAD 遇上“活”的零件)OpenGL渲染与几何内核那点事-项目实践理论补充(一-2-(1)-当你的CAD想“联…...