普通视图
flutter学习第 14 节:动画与过渡效果
flutter学习第 13 节:本地存储
在移动应用开发中,本地存储是一项关键功能,用于保存用户偏好设置、离线数据、登录状态等信息。Flutter 提供了多种本地存储方案,适用于不同的场景需求。本节课将介绍 Flutter 中常用的本地存储方式,包括轻量级的键值对存储和更复杂的数据库存储,并通过实例展示其实际应用。
一、本地存储概述
本地存储在移动应用中具有重要作用,主要应用场景包括:
- 保存用户登录状态,避免重复登录
- 存储应用配置和用户偏好设置
- 缓存网络请求数据,实现离线功能
- 存储结构化数据,如聊天记录、任务列表等
Flutter 中常用的本地存储方案有:
- shared_preferences:轻量级键值对存储,适用于简单数据
- sqflite:SQLite 数据库封装,适用于结构化数据
- hive:高性能 NoSQL 数据库,纯 Dart 实现
- flutter_secure_storage:安全存储,适用于敏感信息如令牌
本节课重点介绍前两种最常用的存储方案。
二、轻量级存储:shared_preferences
shared_preferences
是 Flutter 社区提供的一个插件,用于存储简单的键值对数据。它在 iOS 上使用 NSUserDefaults
,在 Android 上使用 SharedPreferences
,提供了跨平台的一致 API。
1. 安装与配置
在 pubspec.yaml
中添加依赖:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.5.3 # 使用最新版本
运行 flutter pub get
安装依赖。
2. 基本使用方法
shared_preferences
支持存储的数据类型包括:String
、int
、double
、bool
和 List<String>
。
基本操作步骤:
- 获取
SharedPreferences
实例 - 使用相应的方法进行数据读写
- 无需手动关闭实例
import 'package:shared_preferences/shared_preferences.dart';
// 保存数据
Future<void> saveData() async {
// 获取实例
final prefs = await SharedPreferences.getInstance();
// 存储不同类型的数据
await prefs.setString('username', 'john_doe');
await prefs.setInt('age', 30);
await prefs.setDouble('height', 1.75);
await prefs.setBool('isPremium', false);
await prefs.setStringList('hobbies', ['reading', 'sports', 'coding']);
}
// 读取数据
Future<void> readData() async {
final prefs = await SharedPreferences.getInstance();
// 读取数据,提供默认值
String username = prefs.getString('username') ?? 'Guest';
int age = prefs.getInt('age') ?? 0;
double height = prefs.getDouble('height') ?? 0.0;
bool isPremium = prefs.getBool('isPremium') ?? false;
List<String> hobbies = prefs.getStringList('hobbies') ?? [];
print('Username: $username');
print('Age: $age');
print('Height: $height');
print('Is Premium: $isPremium');
print('Hobbies: $hobbies');
}
// 更新数据
Future<void> updateData() async {
final prefs = await SharedPreferences.getInstance();
// 更新已存在的键的值
await prefs.setInt('age', 31);
await prefs.setBool('isPremium', true);
}
// 删除数据
Future<void> deleteData() async {
final prefs = await SharedPreferences.getInstance();
// 删除指定键
await prefs.remove('height');
// 清除所有数据
// await prefs.clear();
}
3. shared_preferences 封装
为了简化使用并避免重复代码,可以封装一个工具类:
import 'package:shared_preferences/shared_preferences.dart';
class PrefsService {
// 单例模式
static final PrefsService _instance = PrefsService._internal();
factory PrefsService() => _instance;
PrefsService._internal();
late SharedPreferences _prefs;
// 初始化
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// 存储方法
Future<void> setString(String key, String value) async {
await _prefs.setString(key, value);
}
Future<void> setInt(String key, int value) async {
await _prefs.setInt(key, value);
}
Future<void> setDouble(String key, double value) async {
await _prefs.setDouble(key, value);
}
Future<void> setBool(String key, bool value) async {
await _prefs.setBool(key, value);
}
Future<void> setStringList(String key, List<String> value) async {
await _prefs.setStringList(key, value);
}
// 读取方法
String getString(String key, {String defaultValue = ''}) {
return _prefs.getString(key) ?? defaultValue;
}
int getInt(String key, {int defaultValue = 0}) {
return _prefs.getInt(key) ?? defaultValue;
}
double getDouble(String key, {double defaultValue = 0.0}) {
return _prefs.getDouble(key) ?? defaultValue;
}
bool getBool(String key, {bool defaultValue = false}) {
return _prefs.getBool(key) ?? defaultValue;
}
List<String> getStringList(String key, {List<String> defaultValue = const []}) {
return _prefs.getStringList(key) ?? defaultValue;
}
// 删除方法
Future<void> remove(String key) async {
await _prefs.remove(key);
}
Future<void> clear() async {
await _prefs.clear();
}
// 检查键是否存在
bool containsKey(String key) {
return _prefs.containsKey(key);
}
}
使用封装类:
// 初始化(通常在 app 启动时)
await PrefsService().init();
// 存储数据
await PrefsService().setString('username', 'jane_smith');
await PrefsService().setInt('score', 100);
// 读取数据
String username = PrefsService().getString('username');
int score = PrefsService().getInt('score');
4. shared_preferences 适用场景与局限性
适用场景:
- 存储用户偏好设置(如主题模式、语言选择)
- 保存简单的用户状态(如登录状态标记)
- 存储少量的配置信息
局限性:
- 不适合存储大量数据
- 不支持复杂数据结构
- 数据存储在明文文件中,不适合存储敏感信息
- 没有查询功能,只能通过键获取值
三、数据库存储:sqflite 基础
对于需要存储大量结构化数据的场景,sqflite
是更好的选择。sqflite
是 Flutter 中 SQLite 数据库的封装,提供了完整的 SQL 操作能力。
1. 安装与配置
在 pubspec.yaml
中添加依赖:
dependencies:
flutter:
sdk: flutter
sqflite: ^2.4.2 # SQLite 数据库
path: ^1.9.1 # 用于处理文件路径
运行 flutter pub get
安装依赖。
2. 数据库基本操作
SQLite 数据库操作主要包括:创建数据库和表、插入数据、查询数据、更新数据和删除数据。
创建数据库和表
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
// 数据库名称
static const _databaseName = "MyDatabase.db";
// 数据库版本
static const _databaseVersion = 1;
// 表名
static const table = 'notes';
// 列名
static const columnId = '_id';
static const columnTitle = 'title';
static const columnContent = 'content';
static const columnCreatedAt = 'created_at';
// 单例模式
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
// 数据库实例
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
// 初始化数据库
_initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(path,
version: _databaseVersion,
onCreate: _onCreate);
}
// 创建表
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $table (
$columnId INTEGER PRIMARY KEY AUTOINCREMENT,
$columnTitle TEXT NOT NULL,
$columnContent TEXT,
$columnCreatedAt TEXT NOT NULL
)
''');
}
}
插入数据
// 插入数据
Future<int> insertNote(Map<String, dynamic> note) async {
Database db = await DatabaseHelper.instance.database;
// 插入数据并返回新记录的 ID
return await db.insert(DatabaseHelper.table, note);
}
// 使用示例
void addNote() async {
Map<String, dynamic> newNote = {
DatabaseHelper.columnTitle: 'First Note',
DatabaseHelper.columnContent: 'This is my first note',
DatabaseHelper.columnCreatedAt: DateTime.now().toIso8601String()
};
int id = await insertNote(newNote);
print('Inserted note with id: $id');
}
查询数据
// 查询所有数据
Future<List<Map<String, dynamic>>> queryAllNotes() async {
Database db = await DatabaseHelper.instance.database;
return await db.query(DatabaseHelper.table);
}
// 根据 ID 查询
Future<List<Map<String, dynamic>>> queryNote(int id) async {
Database db = await DatabaseHelper.instance.database;
return await db.query(DatabaseHelper.table,
where: '${DatabaseHelper.columnId} = ?',
whereArgs: [id]);
}
// 使用示例
void getNotes() async {
List<Map<String, dynamic>> notes = await queryAllNotes();
print('All notes: $notes');
if (notes.isNotEmpty) {
List<Map<String, dynamic>> singleNote = await queryNote(notes[0][DatabaseHelper.columnId]);
print('First note: $singleNote');
}
}
更新数据
// 更新数据
Future<int> updateNote(Map<String, dynamic> note) async {
Database db = await DatabaseHelper.instance.database;
int id = note[DatabaseHelper.columnId];
// 返回受影响的行数
return await db.update(DatabaseHelper.table, note,
where: '${DatabaseHelper.columnId} = ?',
whereArgs: [id]);
}
// 使用示例
void modifyNote(int noteId) async {
Map<String, dynamic> updatedNote = {
DatabaseHelper.columnId: noteId,
DatabaseHelper.columnTitle: 'Updated Note',
DatabaseHelper.columnContent: 'This note has been updated',
DatabaseHelper.columnCreatedAt: DateTime.now().toIso8601String()
};
int rowsAffected = await updateNote(updatedNote);
print('Updated $rowsAffected rows');
}
删除数据
// 删除数据
Future<int> deleteNote(int id) async {
Database db = await DatabaseHelper.instance.database;
// 返回受影响的行数
return await db.delete(DatabaseHelper.table,
where: '${DatabaseHelper.columnId} = ?',
whereArgs: [id]);
}
// 使用示例
void removeNote(int noteId) async {
int rowsDeleted = await deleteNote(noteId);
print('Deleted $rowsDeleted rows');
}
3. 数据库版本迁移
当应用升级需要修改数据库结构时,需要进行数据库迁移:
// 在 DatabaseHelper 类中修改 _initDatabase 方法
_initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade, // 添加升级回调
onDowngrade: onDatabaseDowngradeDelete, // 降级时删除数据库
);
}
// 数据库升级
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// 版本 2 新增了一个 'category' 列
await db.execute('''
ALTER TABLE ${DatabaseHelper.table}
ADD COLUMN category TEXT
''');
}
if (oldVersion < 3) {
// 版本 3 新增了一个 'priority' 列
await db.execute('''
ALTER TABLE ${DatabaseHelper.table}
ADD COLUMN priority INTEGER DEFAULT 0
''');
}
}
4. 模型类与数据库操作封装
为了更好地管理数据,通常会创建模型类并封装数据库操作:
// 笔记模型类
class Note {
final int? id;
final String title;
final String? content;
final String createdAt;
final String? category;
final int priority;
Note({
this.id,
required this.title,
this.content,
required this.createdAt,
this.category,
this.priority = 0,
});
// 从 Map 转换为 Note 对象
factory Note.fromMap(Map<String, dynamic> map) {
return Note(
id: map[DatabaseHelper.columnId],
title: map[DatabaseHelper.columnTitle],
content: map[DatabaseHelper.columnContent],
createdAt: map[DatabaseHelper.columnCreatedAt],
category: map['category'],
priority: map['priority'] ?? 0,
);
}
// 转换为 Map
Map<String, dynamic> toMap() {
return {
DatabaseHelper.columnId: id,
DatabaseHelper.columnTitle: title,
DatabaseHelper.columnContent: content,
DatabaseHelper.columnCreatedAt: createdAt,
'category': category,
'priority': priority,
};
}
}
// 封装数据库操作
class NoteService {
// 插入笔记
Future<int> insertNote(Note note) async {
Database db = await DatabaseHelper.instance.database;
return await db.insert(DatabaseHelper.table, note.toMap());
}
// 获取所有笔记
Future<List<Note>> getAllNotes() async {
Database db = await DatabaseHelper.instance.database;
List<Map<String, dynamic>> maps = await db.query(DatabaseHelper.table);
return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
}
// 根据 ID 获取笔记
Future<Note?> getNoteById(int id) async {
Database db = await DatabaseHelper.instance.database;
List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.table,
where: '${DatabaseHelper.columnId} = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return Note.fromMap(maps.first);
}
return null;
}
// 更新笔记
Future<int> updateNote(Note note) async {
Database db = await DatabaseHelper.instance.database;
return await db.update(
DatabaseHelper.table,
note.toMap(),
where: '${DatabaseHelper.columnId} = ?',
whereArgs: [note.id],
);
}
// 删除笔记
Future<int> deleteNote(int id) async {
Database db = await DatabaseHelper.instance.database;
return await db.delete(
DatabaseHelper.table,
where: '${DatabaseHelper.columnId} = ?',
whereArgs: [id],
);
}
// 按类别查询笔记
Future<List<Note>> getNotesByCategory(String category) async {
Database db = await DatabaseHelper.instance.database;
List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.table,
where: 'category = ?',
whereArgs: [category],
);
return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
}
// 按优先级排序查询
Future<List<Note>> getNotesSortedByPriority() async {
Database db = await DatabaseHelper.instance.database;
List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.table,
orderBy: 'priority DESC',
);
return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
}
}
四、实例:保存用户登录状态
下面实现一个保存用户登录状态的功能,使用 shared_preferences
存储登录信息:
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
static const _tokenKey = 'auth_token';
static const _userIdKey = 'user_id';
static const _usernameKey = 'username';
// 保存登录状态
Future<void> saveLoginState({
required String token,
required String userId,
required String username,
}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_tokenKey, token);
await prefs.setString(_userIdKey, userId);
await prefs.setString(_usernameKey, username);
}
// 获取当前登录用户信息
Future<Map<String, String>?> getCurrentUser() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(_tokenKey);
final userId = prefs.getString(_userIdKey);
final username = prefs.getString(_usernameKey);
if (token != null && userId != null && username != null) {
return {
'token': token,
'userId': userId,
'username': username,
};
}
return null;
}
// 检查是否已登录
Future<bool> isLoggedIn() async {
final user = await getCurrentUser();
return user != null;
}
// 登出,清除登录状态
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
await prefs.remove(_userIdKey);
await prefs.remove(_usernameKey);
}
// 获取认证令牌
Future<String?> getAuthToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_tokenKey);
}
}
在应用启动时检查登录状态:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 检查登录状态
AuthService authService = AuthService();
bool isLoggedIn = await authService.isLoggedIn();
runApp(MyApp(isLoggedIn: isLoggedIn));
}
class MyApp extends StatelessWidget {
final bool isLoggedIn;
const MyApp({super.key, required this.isLoggedIn});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Local Storage Demo',
home: isLoggedIn ? const HomeScreen() : const LoginScreen(),
);
}
}
登录页面实现:
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _authService = AuthService();
bool _isLoading = false;
Future<void> _login(BuildContext context) async {
setState(() {
_isLoading = true;
});
// 模拟 API 登录请求
await Future.delayed(const Duration(seconds: 2));
// 实际应用中应该验证用户名和密码
if (_usernameController.text.isNotEmpty &&
_passwordController.text.isNotEmpty) {
// 保存登录状态
await _authService.saveLoginState(
token: 'fake_auth_token_123456',
userId: 'user_123',
username: _usernameController.text,
);
// 导航到主页
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
);
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter username and password')),
);
}
}
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _usernameController,
decoration: const InputDecoration(labelText: 'Username'),
),
TextField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
const SizedBox(height: 20),
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: () => _login(context),
child: const Text('Login'),
),
],
),
),
);
}
}
主页实现:
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _authService = AuthService();
String? _username;
@override
void initState() {
super.initState();
_loadUserInfo();
}
Future<void> _loadUserInfo() async {
final user = await _authService.getCurrentUser();
setState(() {
_username = user?['username'];
});
}
Future<void> _logout(BuildContext context) async {
await _authService.logout();
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => _logout(context),
),
],
),
body: Center(
child: _username != null
? Text('Welcome, $_username! You are logged in.')
: const Text('Loading user info...'),
),
);
}
}
五、实例:离线数据缓存
使用 sqflite
实现网络数据的本地缓存,实现离线查看功能:
// 新闻模型类
class Article {
final String id;
final String title;
final String? description;
final String url;
final String? urlToImage;
final String publishedAt;
final String? content;
Article({
required this.id,
required this.title,
this.description,
required this.url,
this.urlToImage,
required this.publishedAt,
this.content,
});
// 从 Map 转换
factory Article.fromMap(Map<String, dynamic> map) {
return Article(
id: map['id'],
title: map['title'],
description: map['description'],
url: map['url'],
urlToImage: map['urlToImage'],
publishedAt: map['publishedAt'],
content: map['content'],
);
}
// 转换为 Map
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'description': description,
'url': url,
'urlToImage': urlToImage,
'publishedAt': publishedAt,
'content': content,
'cachedAt': DateTime.now().toIso8601String(), // 缓存时间
};
}
}
// 新闻数据库帮助类
class NewsDatabaseHelper {
static const _databaseName = "NewsDatabase.db";
static const _databaseVersion = 1;
static const table = 'articles';
NewsDatabaseHelper._privateConstructor();
static final NewsDatabaseHelper instance =
NewsDatabaseHelper._privateConstructor();
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
_initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
);
}
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $table (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
url TEXT NOT NULL,
urlToImage TEXT,
publishedAt TEXT NOT NULL,
content TEXT,
cachedAt TEXT NOT NULL
)
''');
}
// 批量插入或更新文章
Future<void> batchInsertOrUpdateArticles(List<Article> articles) async {
Database db = await instance.database;
Batch batch = db.batch();
for (var article in articles) {
batch.insert(
table,
article.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, // 存在则替换
);
}
await batch.commit();
}
// 获取所有缓存文章
Future<List<Article>> getCachedArticles() async {
Database db = await instance.database;
List<Map<String, dynamic>> maps = await db.query(
table,
orderBy: 'publishedAt DESC',
);
return List.generate(maps.length, (i) => Article.fromMap(maps[i]));
}
// 获取单篇文章
Future<Article?> getArticleById(String id) async {
Database db = await instance.database;
List<Map<String, dynamic>> maps = await db.query(
table,
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return Article.fromMap(maps.first);
}
return null;
}
// 清除过期缓存(例如超过7天的)
Future<void> clearExpiredCache() async {
Database db = await instance.database;
final sevenDaysAgo = DateTime.now()
.subtract(const Duration(days: 7))
.toIso8601String();
await db.delete(table, where: 'cachedAt < ?', whereArgs: [sevenDaysAgo]);
}
// 清除所有缓存
Future<void> clearAllCache() async {
Database db = await instance.database;
await db.delete(table);
}
}
新闻仓库类,整合网络请求和本地缓存:
import 'package:dio/dio.dart';
class NewsRepository {
final Dio _dio = Dio();
final NewsDatabaseHelper _dbHelper = NewsDatabaseHelper.instance;
final String _apiKey = 'your_news_api_key';
// 获取头条新闻,优先从网络获取,失败则使用缓存
Future<List<Article>> getTopHeadlines({bool forceRefresh = false}) async {
try {
// 如果不是强制刷新,先检查缓存
if (!forceRefresh) {
final cachedArticles = await _dbHelper.getCachedArticles();
if (cachedArticles.isNotEmpty) {
return cachedArticles;
}
}
// 网络请求
Response response = await _dio.get(
'https://newsapi.org/v2/top-headlines',
queryParameters: {
'country': 'us',
'apiKey': _apiKey,
},
);
// 解析数据
List<Article> articles = (response.data['articles'] as List)
.map((item) => Article(
id: item['url'], // 使用 url 作为唯一标识
title: item['title'],
description: item['description'],
url: item['url'],
urlToImage: item['urlToImage'],
publishedAt: item['publishedAt'],
content: item['content'],
))
.toList();
// 缓存到本地数据库
await _dbHelper.batchInsertOrUpdateArticles(articles);
// 清除过期缓存
await _dbHelper.clearExpiredCache();
return articles;
} catch (e) {
// 网络请求失败,尝试返回缓存
final cachedArticles = await _dbHelper.getCachedArticles();
if (cachedArticles.isNotEmpty) {
return cachedArticles;
}
// 没有缓存,抛出错误
throw Exception('Failed to fetch news and no cache available');
}
}
}
六、其他存储方案简介
1. flutter_secure_storage
用于存储敏感信息,如令牌、密码等,数据会被加密存储:
dependencies:
flutter_secure_storage: ^9.2.4
使用示例:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
// 存储敏感数据
await storage.write(key: 'auth_token', value: 'sensitive_token_123');
// 读取数据
String? token = await storage.read(key: 'auth_token');
// 删除数据
await storage.delete(key: 'auth_token');
2. Hive
Hive 是一个高性能、轻量级的 NoSQL 数据库,纯 Dart 实现,速度快且易于使用:
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
使用示例:
// 初始化
await Hive.initFlutter();
Hive.registerAdapter(NoteAdapter()); // 注册适配器
await Hive.openBox<Note>('notes');
// 获取盒子
var box = Hive.box<Note>('notes');
// 添加数据
var note = Note(id: 1, title: 'Hive Demo', content: 'Hello Hive');
await box.put(note.id, note);
// 获取数据
Note? savedNote = box.get(1);
// 查询所有数据
List<Note> allNotes = box.values.toList();
// 更新数据
note.title = 'Updated Title';
await box.put(note.id, note);
// 删除数据
await box.delete(1);
七、本地存储最佳实践
-
选择合适的存储方案:
- 简单键值对数据使用
shared_preferences
- 敏感信息使用
flutter_secure_storage
- 结构化数据使用
sqflite
或hive
- 简单键值对数据使用
-
数据分层管理:
- 封装存储操作,与业务逻辑分离
- 使用仓库模式(Repository Pattern)整合网络和本地存储
-
性能优化:
- 批量操作数据库,减少 IO 次数
- 及时清理过期缓存,避免存储空间过大
- 数据库操作放在后台线程,避免阻塞 UI
-
错误处理:
- 对所有存储操作添加异常捕获
- 网络请求失败时,提供使用缓存数据的降级策略
-
数据迁移:
- 规划好数据库版本,做好升级迁移方案
- 重大更新时考虑数据备份和恢复机制
-
安全考虑:
- 敏感数据必须加密存储
- 避免存储不必要的用户隐私数据
flutter学习第 11 节:状态管理进阶:Provider
在 Flutter 应用开发中,随着应用规模的扩大,组件之间的状态共享和通信会变得越来越复杂。当多个组件需要访问和修改同一状态时,传统的通过构造函数传递参数的方式会导致代码冗余、耦合度高且难以维护。本节课将介绍 Flutter 中常用的状态管理方案 ——Provider,它基于 InheritedWidget 实现,能够简洁高效地解决跨组件状态共享问题。
一、为什么需要状态管理
在 Flutter 中,所有 UI 都是由 Widget 构成的,而 Widget 是不可变的,状态(State)则是 Widget 中可以变化的数据。当应用简单时,我们可以通过 setState()
管理单个组件的状态,但当状态需要在多个组件之间共享时,就会面临以下问题:
- 跨组件通信困难:深层嵌套的子组件需要访问父组件的状态时,必须通过层层传递参数,形成 "prop drilling" 问题
- 状态同步复杂:多个组件依赖同一状态时,手动同步状态会导致代码逻辑混乱
- 代码可维护性差:状态分散在各个组件中,修改和调试变得困难
- 性能问题:不合理的状态管理会导致不必要的组件重建,影响应用性能
举个例子,一个电商应用中的购物车状态可能需要在商品列表、购物车页面、结算页面等多个地方访问和修改。如果使用传统方式,需要在各个页面之间传递购物车数据,当购物车发生变化时,还要手动通知所有相关页面更新,这显然是低效且容易出错的。
状态管理方案正是为了解决这些问题而诞生的,Provider 是其中最简单易用且官方推荐的方案之一。
二、Provider 基本概念与核心类
Provider 是基于 Flutter 原生 InheritedWidget
实现的状态管理库,它的核心思想是将共享状态抽离到一个独立的类中,然后通过一个 Provider 组件在 Widget 树中提供这个状态,最后在需要使用该状态的子组件中进行消费。
1. 核心类介绍
-
ChangeNotifier:一个实现了观察者模式的类,当状态发生变化时,调用
notifyListeners()
方法通知所有监听者更新 -
ChangeNotifierProvider:一个 Widget,用于在 Widget 树中提供
ChangeNotifier
实例,使其子树中的 Widget 可以访问该实例 -
Consumer:用于在子 Widget 中获取并监听
ChangeNotifier
实例,当状态变化时会重建自身 -
Provider.of:另一种获取
ChangeNotifier
实例的方法,可以选择是否监听状态变化
2. 安装 Provider
在使用 Provider 之前,需要在 pubspec.yaml
中添加依赖:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.5 # 请使用最新版本
然后运行 flutter pub get
安装依赖。
三、Provider 基本使用流程
使用 Provider 管理状态通常遵循以下步骤:
- 创建一个继承自
ChangeNotifier
的状态类,封装需要共享的状态和修改状态的方法 - 使用
ChangeNotifierProvider
在 Widget 树的适当位置提供该状态实例 - 在需要使用状态的子 Widget 中,通过
Consumer
或Provider.of
获取状态并使用
下面通过一个简单的计数器示例来演示基本用法:
1. 创建状态类
import 'package:flutter/foundation.dart';
// 继承 ChangeNotifier
class Counter with ChangeNotifier {
int _count = 0;
// 提供获取状态的方法
int get count => _count;
// 提供修改状态的方法
void increment() {
_count++;
// 通知所有监听者
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}
2. 提供状态
在 Widget 树中使用 ChangeNotifierProvider
提供状态,通常放在应用的根部:
import 'package:provider/provider.dart';
void main() {
runApp(
// 提供 Counter 实例
ChangeNotifierProvider(
create: (context) => Counter(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider Demo',
home: const CounterPage(),
);
}
}
3. 消费状态
在子 Widget 中通过 Consumer
或 Provider.of
获取并使用状态:
使用 Consumer
import 'package:provider/provider.dart';
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter Demo')),
body: Center(
// 使用 Consumer 获取 Counter 实例
child: Consumer<Counter>(
builder: (context, counter, child) {
// builder 方法会在状态变化时重建
return Text(
'Current count: ${counter.count}',
style: const TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
// 使用 Provider.of 获取实例(不监听变化)
Provider.of<Counter>(context, listen: false).decrement();
},
child: const Icon(Icons.remove),
),
const SizedBox(width: 10),
FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).increment();
},
child: const Icon(Icons.add),
),
],
),
);
}
}
Consumer 性能优化
Consumer
的 child
参数可以用于优化性能,避免不必要的重建:
Consumer<Counter>(
builder: (context, counter, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 这个 Text 会在状态变化时重建
Text('Count: ${counter.count}'),
// 这个子组件不会重建,因为它被提取到了 child 参数中
child!,
],
);
},
// 这个子组件只会构建一次
child: const Text('This is a static text'),
)
使用 Provider.of
Provider.of<T>(context)
会获取最近的 T
类型的 Provider,并在状态变化时重建当前 Widget:
// 监听状态变化,状态变化时会重建当前 Widget
final counter = Provider.of<Counter>(context);
// 不监听状态变化,通常用于修改状态的场景
final counter = Provider.of<Counter>(context, listen: false);
使用示例:
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
// 获取 Counter 实例并监听变化
final counter = Provider.of<Counter>(context);
return Text(
'Count: ${counter.count}',
style: const TextStyle(fontSize: 24),
);
}
}
class CounterActions extends StatelessWidget {
const CounterActions({super.key});
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
// 获取 Counter 实例但不监听变化
Provider.of<Counter>(context, listen: false).increment();
},
child: const Icon(Icons.add),
);
}
}
四、多个状态管理
当应用中有多个独立的状态需要管理时,可以使用 MultiProvider
来组织多个 Provider:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => Counter()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => UserProvider()),
],
child: const MyApp(),
),
);
}
这样,在子组件中可以分别获取不同的状态:
// 获取计数器状态
final counter = Provider.of<Counter>(context);
// 获取主题状态
final themeProvider = Provider.of<ThemeProvider>(context);
五、实例:用 Provider 管理购物车状态
下面通过一个完整的购物车示例,展示如何使用 Provider 管理复杂状态:
1. 定义数据模型
// 商品模型
class Product {
final String id;
final String name;
final double price;
final String imageUrl;
Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
}
// 购物车项模型
class CartItem {
final String id;
final String productId;
final String name;
final int quantity;
final double price;
CartItem({
required this.id,
required this.productId,
required this.name,
required this.quantity,
required this.price,
});
// 计算小计
double get totalPrice => price * quantity;
}
2. 创建购物车状态管理类
import 'package:flutter/foundation.dart';
import 'dart:collection';
class Cart with ChangeNotifier {
// 存储购物车项,键为商品ID
final Map<String, CartItem> _items = {};
// 提供不可修改的购物车项视图
UnmodifiableMapView<String, CartItem> get items => UnmodifiableMapView(_items);
// 获取购物车项数量
int get itemCount => _items.length;
// 计算购物车总价
double get totalAmount {
var total = 0.0;
_items.forEach((key, cartItem) {
total += cartItem.price * cartItem.quantity;
});
return total;
}
// 添加商品到购物车
void addItem(Product product) {
if (_items.containsKey(product.id)) {
// 如果商品已在购物车中,增加数量
_items.update(
product.id,
(existingItem) => CartItem(
id: existingItem.id,
productId: existingItem.productId,
name: existingItem.name,
quantity: existingItem.quantity + 1,
price: existingItem.price,
),
);
} else {
// 如果商品不在购物车中,添加新项
_items.putIfAbsent(
product.id,
() => CartItem(
id: DateTime.now().toString(),
productId: product.id,
name: product.name,
quantity: 1,
price: product.price,
),
);
}
notifyListeners();
}
// 从购物车中移除商品
void removeItem(String productId) {
_items.remove(productId);
notifyListeners();
}
// 减少购物车中商品的数量
void removeSingleItem(String productId) {
if (!_items.containsKey(productId)) {
return;
}
if (_items[productId]!.quantity > 1) {
_items.update(
productId,
(existingItem) => CartItem(
id: existingItem.id,
productId: existingItem.productId,
name: existingItem.name,
quantity: existingItem.quantity - 1,
price: existingItem.price,
),
);
} else {
_items.remove(productId);
}
notifyListeners();
}
// 清空购物车
void clear() {
_items.clear();
notifyListeners();
}
}
3. 创建商品列表状态类
class Products with ChangeNotifier {
final List<Product> _items = [
Product(
id: 'p1',
name: 'Red Shirt',
price: 29.99,
imageUrl: 'https://picsum.photos/200/300?random=1',
),
Product(
id: 'p2',
name: 'Trousers',
price: 59.99,
imageUrl: 'https://picsum.photos/200/300?random=2',
),
Product(
id: 'p3',
name: 'Yellow Scarf',
price: 19.99,
imageUrl: 'https://picsum.photos/200/300?random=3',
),
Product(
id: 'p4',
name: 'A Shoes',
price: 99.99,
imageUrl: 'https://picsum.photos/200/300?random=4',
),
];
// 获取商品列表
List<Product> get items => [..._items];
}
4. 在应用中提供状态
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (ctx) => Products()),
ChangeNotifierProvider(create: (ctx) => Cart()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shopping Cart Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const ProductsOverviewScreen(),
routes: {
CartScreen.routeName: (ctx) => const CartScreen(),
},
);
}
}
5. 实现商品列表页面
class ProductsOverviewScreen extends StatelessWidget {
const ProductsOverviewScreen({super.key});
@override
Widget build(BuildContext context) {
// 获取商品列表
final productsData = Provider.of<Products>(context);
final products = productsData.items;
return Scaffold(
appBar: AppBar(
title: const Text('My Shop'),
actions: [
// 购物车图标,显示商品数量
Consumer<Cart>(
builder: (ctx, cart, ch) =>
Badge(label: Text(cart.itemCount.toString()), child: ch!),
child: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
Navigator.of(context).pushNamed(CartScreen.routeName);
},
),
),
],
),
body: GridView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3 / 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemBuilder: (ctx, i) => ProductItem(products[i]),
),
);
}
}
class ProductItem extends StatelessWidget {
final Product product;
const ProductItem(this.product, {super.key});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: GridTile(
footer: GridTileBar(
backgroundColor: Colors.black87,
title: Text(product.name, textAlign: TextAlign.center),
trailing: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
// 将商品添加到购物车
Provider.of<Cart>(context, listen: false).addItem(product);
// 显示添加成功提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Added item to cart!'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
Provider.of<Cart>(
context,
listen: false,
).removeSingleItem(product.id);
},
),
),
);
},
color: Theme.of(context).colorScheme.secondary,
),
),
child: Image.network(product.imageUrl, fit: BoxFit.cover),
),
);
}
}
6. 实现购物车页面
class CartScreen extends StatelessWidget {
static const routeName = '/cart';
const CartScreen({super.key});
@override
Widget build(BuildContext context) {
final cart = Provider.of<Cart>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Your Cart'),
),
body: Column(
children: [
Card(
margin: const EdgeInsets.all(15),
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total', style: TextStyle(fontSize: 20)),
const Spacer(),
Chip(
label: Text(
'$${cart.totalAmount.toStringAsFixed(2)}',
style: TextStyle(
color: Theme.of(context).primaryTextTheme.titleLarge?.color,
),
),
backgroundColor: Theme.of(context).primaryColor,
),
TextButton(
onPressed: () {
// 这里可以添加结算逻辑
},
child: const Text('ORDER NOW'),
)
],
),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (ctx, i) {
final cartItem = cart.items.values.toList()[i];
return ListTile(
leading: CircleAvatar(
child: FittedBox(
child: Text('$${cartItem.price}'),
),
),
title: Text(cartItem.name),
subtitle: Text('Total: $${(cartItem.price * cartItem.quantity).toStringAsFixed(2)}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
cart.removeSingleItem(cartItem.productId);
},
),
Text('${cartItem.quantity}'),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
// 这里可以添加增加商品数量的逻辑
// 简化示例中暂不实现
},
),
],
),
);
},
),
)
],
),
);
}
}
六、Provider 高级用法
1. Selector 优化重建
Selector
是 Consumer
的进阶版本,它可以根据指定的条件决定是否重建,进一步优化性能:
Selector<Counter, int>(
// 选择需要监听的状态
selector: (context, counter) => counter.count,
// 只有当选中的状态变化时才会重建
builder: (context, count, child) {
return Text('Count: $count');
},
)
Selector
接收两个泛型参数:第一个是 ChangeNotifier
类型,第二个是需要监听的状态类型。只有当 selector
方法返回的值发生变化时,builder
才会被调用。
2. 状态持久化
结合 shared_preferences 或 hive 等本地存储库,可以实现状态的持久化:
class Counter with ChangeNotifier {
int _count = 0;
final SharedPreferences _prefs;
Counter(this._prefs) {
// 从本地存储加载状态
_count = _prefs.getInt('count') ?? 0;
}
int get count => _count;
void increment() {
_count++;
// 保存状态到本地存储
_prefs.setInt('count', _count);
notifyListeners();
}
}
// 提供带持久化的状态
ChangeNotifierProvider(
create: (context) => Counter(SharedPreferences.getInstance()),
child: MyApp(),
)
3. 状态封装与业务逻辑分离
对于复杂应用,建议将业务逻辑与状态管理分离,保持 ChangeNotifier
的简洁:
// 业务逻辑层
class CartService {
Future<void> addToCart(Product product) async {
// 处理添加到购物车的业务逻辑,如网络请求等
await Future.delayed(const Duration(milliseconds: 300));
}
}
// 状态管理层
class Cart with ChangeNotifier {
final CartService _cartService;
final List<CartItem> _items = [];
Cart(this._cartService);
List<CartItem> get items => [..._items];
Future<void> addItem(Product product) async {
try {
// 调用业务逻辑
await _cartService.addToCart(product);
// 更新状态
_items.add(CartItem(...));
notifyListeners();
} catch (e) {
// 处理错误
rethrow;
}
}
}
七、Provider 使用最佳实践
- 状态粒度适中:避免创建过大的状态类,应根据功能模块拆分状态
-
最小重建原则:使用
Consumer
和Selector
限制重建范围,避免不必要的重建 - 单一职责:每个状态类应只负责管理相关的一组状态,遵循单一职责原则
- 不可变数据:在状态类中,对于复杂数据结构,建议使用不可变对象,通过替换整个对象来更新状态
-
避免在 build 方法中创建状态:确保
ChangeNotifier
实例的创建在create
方法中,而不是在 build 方法中 -
清理资源:如果状态类中使用了需要手动释放的资源(如定时器),应在
dispose
方法中清理
class TimerModel with ChangeNotifier {
late Timer _timer;
int _seconds = 0;
TimerModel() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_seconds++;
notifyListeners();
});
}
int get seconds => _seconds;
@override
void dispose() {
// 清理资源
_timer.cancel();
super.dispose();
}
}