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

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

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

  • SharedPreferences:轻量键值对存储(首选,适配 80% 场景);
  • 文件存储:本地文件 / 图片存储(适配大文件、自定义格式数据);
  • SQLite:本地关系型数据库(适配大量结构化数据,如本地列表、离线数据库);

同时结合前几节课的Dio 网络请求状态管理,实现登录状态持久化「网络请求→本地缓存→全局共享→重启保留」的全流程闭环,贴合企业真实开发场景。

课前回顾

  1. 网络请求:Dio 全局配置、GET/POST、拦截器统一处理 token;
  2. 状态管理:InheritedWidget 实现全局状态共享(用户信息);
  3. 前置基础:async/await异步操作、实体类序列化(json_serializable);
  4. 核心需求:本地存储的核心是持久化——APP 重启、手机关机后数据不丢失。

一、本地存储方案选型指南

Flutter 的本地存储方案均基于原生端实现(Android/iOS 各自的存储方案,Flutter 封装统一 API),无需关心原生差异,开发时按数据类型、数据量、访问效率选型即可,以下是企业开发的标准选型原则:

表格

存储方案底层实现数据格式适用场景数据量核心优势核心劣势
SharedPreferencesAndroid:SP / iOS:NSUserDefaults键值对(String/int/bool/double/List)登录 token、用户信息、APP 配置、开关状态、轻量缓存小(KB 级)用法简单、API 友好、跨平台统一不支持复杂对象、数据量大会卡顿
文件存储Android/iOS:本地文件系统(沙盒)自定义(文本 / JSON / 二进制 / 图片)大文件、图片 / 视频缓存、自定义格式数据、日志文件中(MB/GB 级)支持任意格式、存储无上限(受手机内存限制)需手动管理文件、解析数据、处理文件路径
SQLiteAndroid/iOS:SQLite 数据库关系型表结构(结构化数据)本地列表、离线数据库、历史记录、大量结构化数据大(GB 级)支持 SQL 查询、事务、索引,访问效率高用法复杂、需建表 / 写 SQL、学习成本高

核心选型原则

  1. 优先使用 SharedPreferences:80% 的本地存储需求(如 token、用户信息、配置项)都能满足,用法最简单,开发效率最高;
  2. 大文件 / 自定义格式用文件存储:如图片缓存、PDF/Excel 文件、自定义日志文件;
  3. 大量结构化数据用 SQLite:如本地商品列表、离线聊天记录、需要分页 / 条件查询的海量数据;
  4. 禁止用 SharedPreferences 存大量 / 复杂数据:如整个列表、嵌套对象,会导致 APP 启动卡顿、存储失败。

二、SharedPreferences:轻量键值对存储(开发首选)

SharedPreferences(简称 SP)是 Flutter最常用、最基础的本地存储方案,适配所有轻量键值对存储场景,官方推荐使用第三方库shared_preferences(Flutter 团队维护,稳定无坑),而非原生 API。

步骤 1:集成依赖

添加最新稳定版依赖到pubspec.yaml,执行flutter pub get安装:

yaml

dependencies: flutter: sdk: flutter shared_preferences: ^2.2.2 # SP核心依赖

步骤 2:封装 SP 工具类(企业级规范)

禁止在页面中直接使用 SPAPI,会导致代码重复、管理混乱,企业开发中会封装全局 SP 工具类,提供统一的增删改查方法,隐藏底层实现,便于后续维护和替换。

创建lib/utils/sp_utils.dart,封装通用方法,支持基本类型JSON 对象(如用户信息实体类)的存储 / 读取:

dart

import 'package:shared_preferences/shared_preferences.dart'; /// SharedPreferences 工具类(单例) class SPUtils { // 单例实例,保证全局唯一 static late SharedPreferences _instance; // 初始化SP,在APP启动时执行(main函数中) static Future<void> init() async { _instance = await SharedPreferences.getInstance(); } // ---------------------- 基本类型操作 ---------------------- static Future<bool> setString(String key, String value) => _instance.setString(key, value); static Future<bool> setInt(String key, int value) => _instance.setInt(key, value); static Future<bool> setBool(String key, bool value) => _instance.setBool(key, value); static Future<bool> setDouble(String key, double value) => _instance.setDouble(key, value); static Future<bool> setStringList(String key, List<String> 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 List<String> getStringList(String key, {List<String> defValue = const []}) => _instance.getStringList(key) ?? defValue; // ---------------------- 自定义对象操作(JSON) ---------------------- // 存储对象:将实体类转为JSON字符串存储 static Future<bool> setObject(String key, Object obj) { String jsonStr = obj is String ? obj : _encodeObjToJson(obj); return setString(key, jsonStr); } // 读取对象:将JSON字符串转为指定类型实体类 static T? getObject<T>(String key, T Function(Map<String, dynamic>) fromJson) { String jsonStr = getString(key); if (jsonStr.isEmpty) return null; return fromJson(_decodeJsonToMap(jsonStr)); } // ---------------------- 通用操作 ---------------------- // 删除指定key static Future<bool> remove(String key) => _instance.remove(key); // 清空所有SP数据 static Future<bool> 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 Map<String, dynamic> _decodeJsonToMap(String jsonStr) { return const JsonDecoder().convert(jsonStr); } }
封装亮点
  1. 单例模式:全局唯一 SP 实例,避免多次初始化;
  2. 基础类型全覆盖:提供 String/int/bool 等所有基础类型的增删改查;
  3. 支持实体类存储:通过JSON 序列化实现复杂对象(如 UserBean)的存储,解决 SP 不支持复杂对象的问题;
  4. 统一初始化:需在 main 函数中初始化,保证使用前 SP 已就绪;
  5. 隐藏底层细节:页面只需调用SPUtils.setString/setObject,无需关心 SP 的底层实现。

步骤 3:在 main 函数中初始化 SP

修改main.dart,在 APP 启动时初始化DioSP,保证全局工具类就绪:

dart

import '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 绑定未初始化」错误。

步骤 4:SP 核心实战 —— 登录状态持久化(核心业务)

结合SP 存储Dio 拦截器InheritedWidget 全局状态,实现企业级登录状态持久化,核心流程:

  1. 登录页:调用登录接口,获取 token 和用户信息;
  2. 本地缓存:将 token 和用户信息(实体类)存入 SP;
  3. 全局共享:更新 InheritedWidget 的全局用户状态;
  4. 请求拦截:Dio 请求拦截器自动从 SP 读取 token,添加到请求头;
  5. APP 重启:启动时从 SP 读取用户信息,初始化全局状态,实现「重启保留登录状态」。
实战 1:定义用户实体类(支持 JSON 序列化)

确保UserBean实现toJson/fromJson(json_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(Map<String, dynamic> json) => _$UserBeanFromJson(json); // 转为JSON Map<String, 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 State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final TextEditingController _phoneController = TextEditingController(); final TextEditingController _pwdController = TextEditingController(); // 模拟登录接口 Future<UserBean> _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), ], ), ), ); } }
实战 3:APP 启动时 —— 从 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 State<AppRoot> createState() => _AppRootState(); } class _AppRootState extends State<AppRoot> { late UserBean _user; @override void initState() { super.initState(); // 初始化:从SP读取用户信息,实现登录状态持久化 _initUserFromSP(); } // 从SP加载用户信息 void _initUserFromSP() { // 从SP读取用户实体类 UserBean? user = SPUtils.getObject<UserBean>("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(), // 根据登录状态渲染初始页面 ); } }
实战 4:Dio 拦截器 —— 从 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;charset=utf-8"}; // 添加拦截器 dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { // 从SP读取用户信息,获取token UserBean? user = SPUtils.getObject<UserBean>("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 核心总结

  1. 封装是必选项:全局 SP 工具类是企业开发的标准,避免代码重复,便于维护;
  2. 复杂对象通过 JSON 存储:SP 本身不支持复杂对象,通过「实体类→JSON 字符串→SP 存储」实现;
  3. 登录状态持久化核心APP启动从SP加载→登录时SP存储+更新全局状态→退出时SP清除+重置状态
  4. 禁止存大量数据:SP 适合轻量数据,大量数据会导致 APP 启动卡顿,建议用 SQLite 替代。

三、文件存储:本地文件 / 图片存储

文件存储适用于大文件、自定义格式数据,Flutter 通过path_provider库获取本地沙盒路径(避免文件权限问题),结合 Dart 原生的io库实现文件的创建、读取、写入、删除,支持文本文件、JSON 文件、二进制文件(图片 / 视频)

核心概念:Flutter 本地沙盒路径

Flutter 的文件存储基于原生沙盒,每个 APP 有独立的沙盒目录,其他 APP 无法访问,保证数据安全,path_provider库提供 3 个核心目录,开发时按需选择:

  1. 应用文档目录(getApplicationDocumentsDirectory):持久化存储,APP 卸载前不会被删除,适合存储用户数据、重要文件;
  2. 临时目录(getTemporaryDirectory):临时存储,系统会自动清理(如低内存时),适合存储缓存、临时文件;
  3. 外部存储目录(getExternalStorageDirectory):仅 Android 支持,适合存储大文件、图片 / 视频(iOS 无此概念)。

步骤 1:集成依赖

需要两个核心依赖:path_provider(获取沙盒路径)、dart:io(Dart 原生文件操作,无需集成):

yaml

dependencies: flutter: sdk: flutter path_provider: ^2.1.1 # 获取本地路径

步骤 2:封装文件存储工具类

创建lib/utils/file_utils.dart,封装路径获取、文本文件操作、图片文件操作的通用方法,隐藏底层路径和 IO 操作:

dart

import 'dart:io'; import 'dart:typed_data'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; /// 文件存储工具类 class FileUtils { // ---------------------- 路径获取 ---------------------- // 获取应用文档目录(持久化,推荐) static Future<Directory> getDocDir() async => await getApplicationDocumentsDirectory(); // 获取临时目录(临时缓存) static Future<Directory> getTempDir() async => await getTemporaryDirectory(); // 拼接文件完整路径:目录+文件名 static Future<String> getFilePath(String fileName, {bool isTemp = false}) async { Directory dir = isTemp ? await getTempDir() : await getDocDir(); return path.join(dir.path, fileName); } // ---------------------- 文本/JSON文件操作 ---------------------- // 写入文本文件(支持JSON字符串) static Future<File> 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 Future<String> 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 Future<File> 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 Future<Uint8List> 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 Future<bool> fileExists(String fileName, {bool isTemp = false}) async { String filePath = await getFilePath(fileName, isTemp: isTemp); return await File(filePath).exists(); } // 删除文件 static Future<bool> 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

// 图片缓存方法:优先从本地读取,本地无则从网络下载并缓存 Future<File> 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 库是sqflite(Flutter 团队维护),结合path_provider获取数据库路径,实现跨平台数据库操作。

核心概念

  1. 数据库:一个 APP 可创建多个数据库,后缀为.db
  2. :数据库的基本单位,按关系型结构存储数据(行 = 记录,列 = 字段);
  3. SQL 语句:实现表的创建、数据的增删改查(CRUD);
  4. 事务:保证多个操作的原子性(要么全部成功,要么全部失败);
  5. 实体类映射:表的字段与实体类的属性一一对应。

步骤 1:集成依赖

需要两个核心依赖:sqflite(SQLite 操作)、path_provider(获取数据库路径):

yaml

dependencies: 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表,实现增删改查:

dart

import '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 Future<void> 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 Future<int> insert(String table, Map<String, dynamic> data) async { return await _db.insert(table, data, conflictAlgorithm: ConflictAlgorithm.replace); } // 查询数据(条件/排序/分页) static Future<List<Map<String, dynamic>>> query( String table, { List<String>? columns, String? where, List<dynamic>? 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 Future<int> update( String table, Map<String, dynamic> data, { String? where, List<dynamic>? whereArgs, }) async { return await _db.update(table, data, where: where, whereArgs: whereArgs); } // 删除数据 static Future<int> delete( String table, { String? where, List<dynamic>? whereArgs, }) async { return await _db.delete(table, where: where, whereArgs: whereArgs); } // 执行原生SQL static Future<void> execute(String sql, [List<dynamic>? arguments]) async { await _db.execute(sql, arguments); } // 开启事务 static Future<T> transaction<T>(Future<T> Function(Transaction txn) action) async { return await _db.transaction(action); } // 关闭数据库 static Future<void> close() async => await _db.close(); // ---------------------- 历史搜索记录专属方法 ---------------------- // 插入搜索记录 static Future<int> insertSearchHistory(String content) async { Map<String, dynamic> data = { "content": content, "time": DateTime.now().millisecondsSinceEpoch, }; return await insert(tableSearchHistory, data); } // 查询所有搜索记录(按时间倒序) static Future<List<Map<String, dynamic>>> getSearchHistory() async { return await query( tableSearchHistory, orderBy: "time DESC", ); } // 删除单条搜索记录 static Future<int> deleteSearchHistory(int id) async { return await delete( tableSearchHistory, where: "id = ?", whereArgs: [id], ); } // 清空所有搜索记录 static Future<int> clearSearchHistory() async { return await delete(tableSearchHistory); } }

步骤 3:初始化数据库

main.dart中添加数据库初始化,与 Dio、SP 一起初始化:

dart

void main() async { WidgetsFlutterBinding.ensureInitialized(); await initDio(); await SPUtils.init(); await DBUtils.init(); // 初始化SQLite runApp(const MyApp()); }

步骤 4:使用数据库 —— 历史搜索记录实战

在搜索页实现添加搜索记录、查询搜索记录、删除搜索记录的功能,直接调用 DBUtils 的专属方法,无需写 SQL 语句:

dart

// 插入搜索记录 await DBUtils.insertSearchHistory("Flutter本地存储"); // 查询所有搜索记录 List<Map<String, dynamic>> historyList = await DBUtils.getSearchHistory(); // 转为实体类列表(可选) List<SearchHistoryModel> 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. 企业开发最佳实践

  1. 统一初始化:Dio、SP、SQLite 在 main 函数中异步初始化,添加WidgetsFlutterBinding.ensureInitialized()
  2. 分层封装:所有本地存储工具类放在lib/utils/,页面只调用方法,不直接操作底层 API;
  3. 实体类映射:SP / 文件 / SQLite 的存储数据与实体类一一对应,禁止直接使用 Map;
  4. 异常处理:所有本地存储操作都是异步的,必须添加try/catch捕获异常;
  5. 资源释放:文件 / 数据库使用完成后及时关闭,避免资源泄漏;

六、课后练习(本地存储必备,贴合企业场景)

  1. 基础练习:基于 SP 实现APP 夜间模式持久化—— 切换夜间模式时存储到 SP,APP 重启后保持夜间模式状态;
  2. 进阶练习:基于文件存储实现用户头像本地缓存—— 从网络下载头像图片,保存到应用文档目录,修改头像时更新本地文件,头像组件优先读取本地文件;
  3. 实战练习:基于 SQLite 实现本地商品列表—— 将网络请求的商品列表存入 SQLite,实现离线查看商品,下拉刷新时同步网络数据更新本地数据库。

下一节课预告

我们会学习 Flutter 的路由与导航进阶(企业级),解决基础路由的痛点:

  • 命名路由:统一管理路由表,实现无耦合的页面跳转;
  • 路由传参:基础类型 / 实体类 / 回调函数传参,解决复杂传参问题;
  • 路由拦截:实现登录拦截(未登录时跳转到登录页)、权限拦截;
  • 页面转场动画:自定义页面跳转 / 返回的动画,提升用户体验;
  • 路由管理:获取当前路由、返回上一页、返回到根页面、关闭所有页面;

路由是 APP 页面跳转的核心,企业开发中必须使用命名路由 + 路由拦截,实现页面的解耦和权限控制,结合之前的登录状态持久化,实现完整的权限控制体系

我可以帮你把本节课的SP 工具类、文件工具类、数据库工具类整合为一个可直接复用的 Flutter 工具库,包含登录状态持久化、图片缓存、历史记录的完整代码,需要吗?

http://www.jsqmd.com/news/662226/

相关文章:

  • 10大好用无代码开发平台测评!企业无代码开发选型必看清单
  • 深度指南:构建现代B站视频下载器的5大核心技术
  • 5分钟玩转tao-8k:Xinference部署+LangChain集成全流程解析
  • 别再只用MIO了!手把手教你用Zynq的EMIO在Vivado 2023.1里点亮PL端的LED
  • 《Hermes Agent 代码库安全漏洞分析与解决办法》
  • 2025年Workout.Cool功能革新:如何打造个性化开源健身教练平台
  • Excel高效办公:一键实现图片名称批量整理与精准匹配
  • 我开源了 27 个思维模型,每周更新,欢迎 Star
  • Outfit字体:重新定义品牌视觉语言的几何美学革命 [特殊字符]
  • C语言数组解析:从定义到内存布局详解
  • Notepad-- 完整使用指南:从零开始掌握跨平台文本编辑利器
  • 【游戏开发进阶】Unity URP技能贴花实战:从ShaderGraph到性能优化的全流程解析
  • 低分辨率图像修复难题的终极解决方案:Upscayl深度技术解析
  • GPU显存终极检测指南:memtest_vulkan让你轻松掌握显卡健康状况
  • 用python解放右手系列(三) Excel自动化-告别复制粘贴的噩梦
  • 2026毕业季实测:6款论文AI工具横评,本科/硕博开题答辩全场景避坑指南
  • 不会命令行,也能管理服务器吗?新手第一次上手 Linux 的更轻松办法
  • COMSOL 超表面仿真:从入门到“光速”出图!
  • Webbrowser控件加载IE不同版本内核-注册表设置
  • WarcraftHelper:让经典魔兽争霸3在现代电脑上焕发新生的终极解决方案
  • Hailo8 Dataflow Compiler 模型转换指南--以 ONNX 模型为例
  • Nacos配置中心隐藏技巧:用JSON配置动态菜单、黑白名单,告别硬编码
  • 保姆级教程:手把手教你正确设置群晖Drive、Moments的个人文件存储权限
  • Qt 5.15 + QMediaPlayer 播放 RTSP 监控流保姆级教程(解决黑屏/报错)
  • 告别手动投稿!用Python轻松实现B站视频批量上传的智能解决方案
  • 【2024 AGI技术成熟度白皮书】:12项核心指标首次量化评估,仅2项达Gartner Hype Cycle峰值前夜
  • MusePublic Art Studio生成多样性控制:潜在空间探索技术
  • FairyGUI按钮动效实战:从点击缩放+音效到复杂转场,一个完整项目案例拆解
  • no-vue3-cron:基于Vue 3.0的可视化Cron表达式生成器深度解析
  • Fish-Speech 1.5新手必看:3个参数调出完美语音,告别重复卡顿