状态管理场景
上一章的例子中,只涉及一个有状态的组件 article_like_bar ,接下来我们需要实现另外一个详情页面,并且在详情页面中也需要一个点赞功能,具体的界面效果可以参考动图 1 (为了界面更好,我在上一章的基础上增加了一些样式)。
在上面的动图例子中,你是否发现了一个问题?第一个页面的点赞数与第二个页面的点赞数并不同步。在实际项目开发过中,需求方希望二级详情页面的点赞数能与第一个页面的点赞数同步。
如果不引入新的技术方案,能想到的办法就是将该状态进行提升,放到其共同的父节点上,然后将父节点设计为有状态组件,并提供修改状态的方法给到子组件。可以用图 2 来表示。
上面的方式是可以做到这点,但是你有没有发现,只因为一个点赞行为,就需要将两个页面的所有组件(静态组件和动图组件)进行重新 build ,成本实在太高,这也违背了我们上一章的组件设计原则(尽可能减少动态组件下的静态组件)。为了更好地解决这个问题,我们就需要引入一些状态管理的方法,下面就介绍一些常见的技术方案,同时做一个对比。
状态管理
Provider
官方推荐的技术方案 Provider ,开发过程比较简单,分为三步:
- 创建状态管理类 name_model ,创建对应的状态 name 以及其修改 name 的方法 changeName;
- 在 name_game 中增加 provider 的支持,并将相应需要共享的组件使用 provider 进行封装,监听数据变化;
- 在子组件中获取 provider 的 name 数据以及 changeName 方法,在相应的点击部分触发 changeName 事件。
在使用 Provider 来实现状态管理,我们需要创建一个 model 文件夹,放入对应的状态类 name_model ,代码实现如下:
import 'dart:math';
import 'package:flutter/material.dart';
/// name状态管理模块
class NameModel with ChangeNotifier {
/// 声明私有变量
String _name = 'test flutter';
/// 设置get方法
String get value => _name;
/// 修改当前name,随机选取一个
void changeName() {
List<String> nameList = ['flutter one', 'flutter two', 'flutter three'];
int pos = Random().nextInt(3);
if(_name != nameList[pos]) {
_name = nameList[pos];
notifyListeners();
}
}
}
在第 6 行代码中,使用了一个 Dart 的 with 关键词,这个用法是表示 NameModel 可以直接调用 ChangeNotifier 的方法,比如第 15 行的代码就是调用了 ChangeNotifier 类中的方法。上面代码中,在 changeName 中设置完状态属性 _name 以后,通过 ChangeNotifier 通知监听方。为了性能优化,在第 18 到第 21 行进行了判断,避免属性未改变而触发 build 操作。接下来看一下,在 name_game 中是如何监听数据变化,代码实现如下:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you/model/name_model.dart';
import 'package:two_you/widgets/name_game/random_name.dart';
import 'package:two_you/widgets/name_game/test_other.dart';
import 'package:two_you/widgets/name_game/welcome.dart';
/// 测试随机名字游戏组件
class NameGame extends StatelessWidget {
/// 设置状态 name
final name = NameModel();
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Provider<String>.value(
child: ChangeNotifierProvider.value(
value: name,
child: Column(
children: <Widget>[
Welcome(),
RandomName(),
],
),
),
),
TestOther(),
],
);
}
}
上述代码中,第 13 行获取状态属性 name ,在 build 逻辑中使用 Provider.value 来封装需要共享的组件,String 为 name 相应的字段类型。并且使用 ChangeNotifierProvider 来接受监听数据变化,当数据发生变化时则触发子组件的 build 。
最后我们再来看其中的一个子组件 RandomName ,在 RandomName 中展示 name 字段,并且有一个按钮触发 changeName 操作,代码实现如下。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you/model/name_model.dart';
/// 随机展示人名
class RandomName extends StatelessWidget {
/// 有状态类返回组件信息
@override
Widget build(BuildContext context) {
final _name = Provider.of<NameModel>(context);
print('random name build');
return FlatButton(
child: Text(_name.value),
onPressed: () => _name.changeName(),
);
}
}
第 11 行通过 Provider.of(context) 方式,获得根节点 NameModel 的句柄,然后通过 NameModel 的 value 获得状态 name 的值,其次使用 _name.changeName 执行 NameModel 的方法,触发 name 状态值的修改,从而再通过 ChangeNotifier 通知到两个组件 welcome 和 random_name 。
以上就完成了整个 Provider 的实现逻辑,相对其他技术方案,则更简洁一些。
使用 Provider 来完善上一章中的例子
创建 like_num_model
import 'package:flutter/material.dart';
/// name状态管理模块
class LikeNumModel with ChangeNotifier {
/// 声明私有变量
int _likeNum = 0;
/// 设置get方法
int get value => _likeNum;
/// 修改当前name,随机选取一个
void like() {
_likeNum++;
notifyListeners();
}
}
由于每次都会自增,因此在 like 函数中无须判断是否 likeNum 状态有变化,只要自增了 likeNum 状态后通知监听方即可。
main 函数创建监听组件
由于涉及两个页面,并不是两个组件,因此这里需要将状态提升到 main 函数中,mian 组件的实现如下:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you/model/like_num_model.dart';
import 'package:two_you/pages/home_page.dart';
/// APP 核心入口文件
void main() {
runApp(MyApp());
}
/// MyApp 核心入口界面
class MyApp extends StatelessWidget {
/// 创建 like model
final likeNumModel = LikeNumModel();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return Provider<int>.value(
child: ChangeNotifierProvider.value(
value: likeNumModel,
child: MaterialApp(
title: 'Two You', // APP 名字
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue, // APP 主题
),
home: Scaffold(
appBar: AppBar(
title: Text('Two You'), // 页面名字
),
body: Center(
child: HomePage(),
))),
),
);
}
}
上述代码第 16 行,创建了状态管理类的对象,并通过 Provider.value 和 ChangeNotifierProvider.value 来封装组件 HomePage ,由于 ArticlePage 也是在页面组件中的 MaterialApp 组件下,因此都可以通过 context 获取 likeNumModel 句柄。
使用 likeNumModel
使用 Provider 的好处就在于,不使用的部分完全不需要修改,只需要在使用该状态的地方修改即可。由于 likeNumModel 只在 article_detail_like 和 article_like_bar 中使用,因此修改这两个组件即可。
article_like_bar 代码如下:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you/model/like_num_model.dart';
import 'package:two_you/styles/text_syles.dart';
/// 帖子文章的赞组件
///
/// 包括点赞组件 icon ,以及组件点击效果
/// 需要外部参数[likeNum],点赞数量
class ArticleLikeBar extends StatelessWidget {
/// 有状态类返回组件信息
@override
Widget build(BuildContext context) {
final likeNumModel = Provider.of<LikeNumModel>(context);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.thumb_up, color: Colors.grey, size: 18),
Padding(padding: EdgeInsets.only(left: 10)),
Text(
'${likeModel.value}',
style: TextStyles.commonStyle(),
),
],
),
onPressed: () => likeNumModel.like(),
),
],
);
}
}
在第 15 行获取操作句柄,然后在第 26 行获取属性 likeNum , 在第 31 行执行 likeNumModel 执行 like 操作。
article_detail_like 代码如下:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you/model/like_num_model.dart';
import 'package:two_you/styles/text_syles.dart';
/// 帖子详情页的赞组件
///
/// 包括点赞组件 icon ,以及组件点击效果
/// 需要外部参数[likeNum],点赞数量
class ArticleDetailLike extends StatelessWidget {
/// 有状态类返回组件信息
@override
Widget build(BuildContext context) {
final likeNumModel = Provider.of<LikeNumModel>(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Icon(Icons.thumb_up, color: Colors.grey, size: 40),
onPressed: () => likeNumModel.like(),
),
Text(
'${likeNumModel.value}',
style: TextStyles.commonStyle(),
),
],
);
}
}
同样上面的第 15 行获取 likeNumModel 操作句柄,然后在第 22 行执行 like 操作,在第 25 行显示点赞数量。
接下来我们运行下项目,可以看到效果如图所示。