
做Flutter开发,状态管理是绑不开的话题。一个按钮点击后,怎么让另一个页面的数据跟着变?用户收藏了一篇文章,怎么让收藏列表实时更新?切换了深色模式,怎么让整个App立刻变色?
这些问题的答案都指向同一个方向:状态管理。
今天咱们就来聊聊,在今日资讯App里是怎么用Provider来管理状态的。不讲那些虚的概念,直接看代码,看实际怎么用。
状态管理到底在管什么
先搞清楚一个问题:什么是"状态"?
简单说,状态就是会变化的数据。比如:
- 当前选中的新闻分类
- 用户收藏的文章列表
- App是深色模式还是浅色模式
- 新闻列表是否正在加载
这些数据有个共同特点:它们会变,而且变了之后UI要跟着变。
Flutter自带的setState能处理简单场景,但当数据需要在多个页面间共享时,setState就不够用了。这时候就需要状态管理方案。
Provider是Flutter官方推荐的状态管理方案之一,简单易用,适合中小型项目。咱们的今日资讯App就用的Provider。
Provider的基本原理
Provider的核心思想很简单:把数据放在Widget树的上层,下层的Widget需要数据时,往上找就行了。
MaterialApp
└── MultiProvider ← 数据放这里
└── HomeScreen
└── NewsList ← 需要数据时往上找
当数据变化时,Provider会通知所有依赖这个数据的Widget重新构建。这就是所谓的"响应式"。
项目中的三个Provider
今日资讯App里有三个Provider,各管各的事:
NewsProvider:管理新闻数据,包括获取、缓存、刷新FavoritesProvider:管理收藏功能,包括添加、删除、持久化ThemeProvider:管理主题,包括深色模式切换
咱们一个一个来看。
NewsProvider:新闻数据的大管家
先看NewsProvider的整体结构:
dart
import 'package:flutter/material.dart';
import '../models/news_article.dart';
import '../services/api_service.dart';
class NewsProvider extends ChangeNotifier {
final ApiService _apiService = ApiService();
final Map<String, List<NewsArticle>> _newsCache = {};
bool _isLoading = false;
String? _error;
bool get isLoading => _isLoading;
String? get error => _error;
这段代码定义了NewsProvider的基本属性。ChangeNotifier是Flutter提供的一个mixin,它提供了notifyListeners()方法,调用这个方法就能通知所有监听者"数据变了,该刷新了"。
_apiService是API服务的实例,所有网络请求都通过它发出。前面加下划线表示私有,外部访问不到。
_newsCache是个Map,用来缓存已经请求过的新闻。key是分类名(比如'tech'、'sports'),value是该分类的新闻列表。为什么要缓存?因为用户可能频繁切换分类,每次都请求网络太慢了,缓存起来切换就是瞬间的事。
_isLoading和_error是加载状态和错误信息,UI层会根据这两个值显示不同的界面。
最后两行是getter,让外部能读取这些私有属性,但不能直接修改。这是封装的基本原则:数据的修改权在Provider内部,外部只能读。
获取指定分类的新闻
dart
List<NewsArticle> getNewsByCategory(String category) {
return _newsCache[category] ?? [];
}
这个方法很简单,从缓存里取数据。如果没有就返回空列表。注意这里用了??空合运算符,如果_newsCache[category]是null,就返回[]。
请求新闻数据
dart
Future<void> fetchNews(String category) async {
if (_newsCache.containsKey(category) && _newsCache[category]!.isNotEmpty) {
return;
}
_isLoading = true;
_error = null;
notifyListeners();
fetchNews是核心方法。一开始先检查缓存,如果缓存里有数据,直接return,不发请求。这个优化很重要,避免了重复请求。
如果缓存没有,就开始请求。先把_isLoading设为true,_error清空,然后调用notifyListeners()。这一步很关键,它会通知UI"我要开始加载了",UI就会显示loading动画。
根据分类调用不同接口
dart
try {
List<NewsArticle> articles;
switch (category) {
case 'space':
articles = await _apiService.fetchSpaceNews();
break;
case 'tech':
articles = await _apiService.fetchTechNews();
break;
case 'sports':
articles = await _apiService.fetchSportsNews();
break;
case 'entertainment':
articles = await _apiService.fetchEntertainmentNews();
break;
case 'business':
articles = await _apiService.fetchBusinessNews();
break;
case 'health':
articles = await _apiService.fetchHealthNews();
break;
case 'science':
articles = await _apiService.fetchScienceNews();
break;
default:
articles = await _apiService.fetchSpaceNews();
}
_newsCache[category] = articles;
用switch根据分类调用不同的API方法。为什么不用一个通用方法?因为不同分类可能对应不同的数据源,有的是真实API,有的是模拟数据。
请求成功后,把数据存入缓存。下次再请求同一个分类,就直接从缓存取了。
错误处理和状态恢复
dart
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
用try-catch捕获异常。网络请求可能因为各种原因失败:网络断开、服务器错误、超时等。捕获后把错误信息存到_error,UI层可以显示给用户。
finally块里把_isLoading设回false,然后notifyListeners()。无论成功还是失败,都要告诉UI"加载结束了"。用finally确保这段代码一定会执行。
刷新新闻
dart
Future<void> refreshNews(String category) async {
_newsCache.remove(category);
await fetchNews(category);
}
刷新就是清缓存再请求。remove删除指定分类的缓存,然后调用fetchNews。因为缓存被清了,fetchNews里的缓存检查会失败,就会发起新请求。
用户下拉刷新时就调用这个方法。
搜索新闻
dart
Future<List<NewsArticle>> searchNews(String query) async {
return await _apiService.searchSpaceNews(query);
}
搜索不走缓存,每次都请求。因为搜索关键词千变万化,缓存意义不大。
FavoritesProvider:收藏功能的实现
收藏功能比新闻列表复杂一点,因为要持久化存储。用户收藏的文章,关掉App再打开还得在。
dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/news_article.dart';
class FavoritesProvider extends ChangeNotifier {
List<NewsArticle> _favorites = [];
List<NewsArticle> get favorites => _favorites;
FavoritesProvider() {
_loadFavorites();
}
_favorites是收藏列表,存的是NewsArticle对象。
构造函数里调用_loadFavorites(),这意味着Provider一创建就会从本地存储加载收藏数据。
从本地加载收藏
dart
Future<void> _loadFavorites() async {
final prefs = await SharedPreferences.getInstance();
final favoritesJson = prefs.getStringList('favorites') ?? [];
_favorites = favoritesJson
.map((json) => NewsArticle.fromJson(jsonDecode(json)))
.toList();
notifyListeners();
}
SharedPreferences是Flutter的本地存储方案,类似于Web的localStorage。它只能存基本类型(String、int、bool、List等),不能直接存对象。
所以存储时要把对象转成JSON字符串,读取时再转回来。jsonDecode把JSON字符串解析成Map,NewsArticle.fromJson把Map转成对象。
最后notifyListeners()通知UI刷新。
保存收藏到本地
dart
Future<void> _saveFavorites() async {
final prefs = await SharedPreferences.getInstance();
final favoritesJson = _favorites
.map((article) => jsonEncode(article.toJson()))
.toList();
await prefs.setStringList('favorites', favoritesJson);
}
保存是加载的逆过程。article.toJson()把对象转成Map,jsonEncode把Map转成JSON字符串。
这个方法是私有的,只在内部调用。每次收藏列表变化时都要调用它,确保数据持久化。
判断是否已收藏
dart
bool isFavorite(String articleId) {
return _favorites.any((article) => article.id == articleId);
}
any方法检查列表中是否有满足条件的元素。这里检查是否有id匹配的文章。
这个方法在UI层很有用,比如显示收藏按钮时,要根据是否已收藏显示不同的图标。
切换收藏状态
dart
Future<void> toggleFavorite(NewsArticle article) async {
if (isFavorite(article.id)) {
_favorites.removeWhere((a) => a.id == article.id);
} else {
_favorites.insert(0, article);
}
await _saveFavorites();
notifyListeners();
}
这是收藏功能的核心方法。如果已收藏就取消,没收藏就添加。
removeWhere删除所有满足条件的元素。insert(0, article)把新收藏插到列表开头,这样最新收藏的在最前面。
操作完成后保存到本地,然后通知UI刷新。
清空收藏
dart
Future<void> clearFavorites() async {
_favorites.clear();
await _saveFavorites();
notifyListeners();
}
清空很简单,clear()清空列表,保存,通知。
ThemeProvider:主题切换的实现
主题切换是个很常见的功能,现在的App基本都支持深色模式。
dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
ThemeProvider() {
_loadThemeMode();
}
ThemeMode是Flutter内置的枚举,有三个值:system(跟随系统)、light(浅色)、dark(深色)。
默认值是system,跟随系统设置。构造函数里加载用户之前的选择。
加载主题设置
dart
Future<void> _loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
final themeModeString = prefs.getString('themeMode') ?? 'system';
_themeMode = ThemeMode.values.firstWhere(
(e) => e.toString() == 'ThemeMode.$themeModeString',
orElse: () => ThemeMode.system,
);
notifyListeners();
}
从SharedPreferences读取存储的主题模式字符串,然后转成ThemeMode枚举。
ThemeMode.values是所有枚举值的列表,firstWhere找到第一个匹配的。orElse是找不到时的默认值。
设置主题模式
dart
Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('themeMode', mode.toString().split('.').last);
notifyListeners();
}
设置新的主题模式,保存到本地,通知UI刷新。
mode.toString().split('.').last把ThemeMode.dark转成dark。存储时只存枚举名,不存完整的字符串。
定义浅色主题
dart
ThemeData get lightTheme => ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
ThemeData定义了整个App的视觉风格。useMaterial3: true启用Material 3设计语言,这是Google最新的设计规范。
ColorScheme.fromSeed根据一个种子颜色自动生成一套配色方案。传入Colors.blue,它会自动生成与蓝色协调的各种颜色。
appBarTheme定义AppBar的样式,centerTitle: true让标题居中,elevation: 0去掉阴影。
cardTheme定义卡片样式,圆角12像素,阴影2。
定义深色主题
dart
ThemeData get darkTheme => ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
深色主题和浅色主题结构一样,只是brightness改成Brightness.dark。ColorScheme.fromSeed会根据brightness自动生成适合深色模式的配色。
在UI中使用Provider
Provider定义好了,怎么在UI里用呢?看看HomeScreen的实现。
初始化时加载数据
dart
class _HomeScreenState extends State<HomeScreen> {
final List<Map<String, dynamic>> _categories = [
{'name': '航天', 'key': 'space', 'icon': Icons.rocket_launch},
{'name': '科技', 'key': 'tech', 'icon': Icons.computer},
{'name': '体育', 'key': 'sports', 'icon': Icons.sports_soccer},
{'name': '娱乐', 'key': 'entertainment', 'icon': Icons.movie},
{'name': '商业', 'key': 'business', 'icon': Icons.business},
{'name': '健康', 'key': 'health', 'icon': Icons.health_and_safety},
{'name': '科学', 'key': 'science', 'icon': Icons.science},
];
String _selectedCategory = 'space';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<NewsProvider>().fetchNews(_selectedCategory);
});
}
_categories定义了所有新闻分类,每个分类有名称、key和图标。
_selectedCategory是当前选中的分类,默认是'space'(航天)。
initState里用addPostFrameCallback延迟执行数据加载。为什么要延迟?因为initState执行时Widget还没完全构建好,直接用context.read可能出问题。addPostFrameCallback确保在第一帧渲染完成后再执行。
context.read<NewsProvider>()获取NewsProvider实例,然后调用fetchNews加载数据。read只获取一次,不会监听变化。
切换分类时加载数据
dart
child: CategoryChip(
label: category['name'],
icon: category['icon'],
isSelected: isSelected,
onTap: () {
setState(() {
_selectedCategory = category['key'];
});
context.read<NewsProvider>().fetchNews(_selectedCategory);
},
),
用户点击分类标签时,先用setState更新本地状态_selectedCategory,然后调用fetchNews加载新分类的数据。
这里用read而不是watch,因为我们只是触发一个动作,不需要监听Provider的变化。
监听数据变化并更新UI
dart
Widget _buildNewsList() {
return Consumer<NewsProvider>(
builder: (context, newsProvider, child) {
if (newsProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (newsProvider.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text('加载失败: ${newsProvider.error}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
newsProvider.refreshNews(_selectedCategory);
},
child: const Text('重试'),
),
],
),
);
}
Consumer是Provider提供的Widget,它会监听指定Provider的变化,变化时自动重建。
builder函数接收三个参数:context、newsProvider(Provider实例)、child(不需要重建的子Widget)。
根据isLoading和error显示不同的UI:加载中显示转圈,出错显示错误信息和重试按钮。
显示新闻列表
dart
final articles = newsProvider.getNewsByCategory(_selectedCategory);
if (articles.isEmpty) {
return const Center(
child: Text('暂无新闻'),
);
}
return RefreshIndicator(
onRefresh: () => newsProvider.refreshNews(_selectedCategory),
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: articles.length,
itemBuilder: (context, index) {
return NewsCard(article: articles[index]);
},
),
);
},
);
}
从Provider获取当前分类的新闻列表,如果为空显示提示,否则显示列表。
RefreshIndicator实现下拉刷新,onRefresh回调调用refreshNews方法。
ListView.builder懒加载列表,只构建可见的item,性能好。
read、watch、Consumer的区别
用Provider时经常会纠结用哪个方法,这里说清楚:
context.read<T>():获取Provider实例,不监听变化。适合在事件处理函数里用,比如按钮点击。
context.watch<T>():获取Provider实例,并监听变化。Provider变化时会触发Widget重建。适合在build方法里用。
Consumer<T>:和watch类似,但可以精确控制重建范围。只有Consumer内部会重建,外部不受影响。
举个例子:
dart
// 整个Widget都会重建
Widget build(BuildContext context) {
final provider = context.watch<NewsProvider>();
return Column(
children: [
Text('Loading: ${provider.isLoading}'),
// 很多其他Widget...
],
);
}
// 只有Consumer内部重建
Widget build(BuildContext context) {
return Column(
children: [
Consumer<NewsProvider>(
builder: (context, provider, child) {
return Text('Loading: ${provider.isLoading}');
},
),
// 这些Widget不会重建
],
);
}
性能敏感的场景用Consumer,简单场景用watch就行。
Provider的注册
Provider要在Widget树的上层注册,通常在main.dart里:
dart
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => NewsProvider()),
ChangeNotifierProvider(create: (_) => FavoritesProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
],
child: const MyApp(),
),
);
}
MultiProvider可以注册多个Provider。ChangeNotifierProvider专门用于ChangeNotifier类型的Provider。
create参数是一个工厂函数,返回Provider实例。用(_)表示不需要context参数。
在MaterialApp中使用ThemeProvider
dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return MaterialApp(
title: '今日资讯',
theme: themeProvider.lightTheme,
darkTheme: themeProvider.darkTheme,
themeMode: themeProvider.themeMode,
home: const MainScreen(),
);
},
);
}
}
用Consumer监听ThemeProvider,主题变化时整个MaterialApp重建,所有页面的主题都会更新。
状态管理的最佳实践
做了这么多,总结几条经验:
把状态放在合适的层级 。如果状态只在一个Widget内部用,用setState就够了。如果要跨页面共享,才用Provider。
Provider要职责单一。NewsProvider只管新闻,FavoritesProvider只管收藏,ThemeProvider只管主题。不要把所有状态塞到一个Provider里。
善用缓存。网络请求是昂贵的操作,能缓存就缓存。但也要提供刷新机制,让用户能获取最新数据。
状态变化要通知 。每次修改状态后都要调用notifyListeners(),否则UI不会更新。这是最常见的bug来源。
异步操作要处理loading和error。用户需要知道"正在加载"和"加载失败",不能让界面卡住没反应。
持久化重要数据。用户的收藏、设置这些数据要存到本地,下次打开App还在。
常见问题排查
问题一:修改了状态但UI不更新
检查是否调用了notifyListeners()。这是最常见的遗漏。
问题二:Provider not found错误
检查Provider是否在Widget树的上层注册了。Consumer/watch只能访问祖先节点的Provider。
问题三:在initState里用context.watch报错
initState里不能用watch,因为Widget还没完全构建好。用addPostFrameCallback延迟执行,或者用read。
问题四:性能问题,整个页面频繁重建
用Consumer缩小重建范围,或者用Selector只监听需要的属性。
写在最后
状态管理是Flutter开发的核心技能之一。Provider虽然简单,但用好了能解决大部分问题。
关键是理解它的原理:数据放在上层,下层监听变化,变化时自动重建。掌握了这个,其他状态管理方案(Riverpod、Bloc、GetX)也能很快上手,因为核心思想是相通的。
今日资讯App的三个Provider各司其职,NewsProvider管数据获取和缓存,FavoritesProvider管收藏和持久化,ThemeProvider管主题切换。这种分工让代码清晰、易维护。
状态管理没有银弹,选择适合项目规模的方案就好。小项目用Provider足够,大项目可能需要更强大的方案。但不管用什么,核心原则都是一样的:单一职责、数据流清晰、UI响应状态变化。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里你可以找到更多Flutter开发资源,与其他开发者交流经验,共同进步。