Flutter for OpenHarmony:从零搭建今日资讯App(二十五)状态管理的艺术与实践

做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('.').lastThemeMode.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.darkColorScheme.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函数接收三个参数:contextnewsProvider(Provider实例)、child(不需要重建的子Widget)。

根据isLoadingerror显示不同的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开发资源,与其他开发者交流经验,共同进步。

相关推荐
IT陈图图2 小时前
基于 Flutter × OpenHarmony 的文本排序工具开发实战
flutter·开源·鸿蒙·openharmony
—Qeyser2 小时前
Flutter 组件通信完全指南
前端·javascript·flutter
奋斗的小青年!!2 小时前
Flutter开发OpenHarmony应用:设置页面组件的深度实践
flutter·harmonyos·鸿蒙
大雷神2 小时前
HarmonyAPP 开发Flutter 嵌入鸿蒙原生 Swiper 组件教程
flutter·华为·harmonyos
时光慢煮3 小时前
基于 Flutter × OpenHarmony 的大小写转换工具实践
flutter·openharmony
—Qeyser4 小时前
Flutter AppBar 导航栏组件完全指南
前端·javascript·flutter
鸣弦artha4 小时前
Flutter框架跨平台鸿蒙开发——Flutter Framework层架构概览
flutter·架构·harmonyos
2401_882351525 小时前
Flutter for OpenHarmony 商城App实战 - 购物车实现
java·flutter·dubbo
嘴贱欠吻!6 小时前
Flutter鸿蒙开发指南(二):组件类型与状态管理
flutter