Flutter 组件的事件传递与数据控制 | 开发者说·DTalk

2020 年 10 月 14 日 谷歌开发者

本文原作者: 马嘉伦,原文发布于 segmentfault

https://segmentfault.com/a/1190000023338363


本文使用原生 Flutter 形式设计代码,只讲最基础的东西,不使用任何其他第三方库 ( Provider 等)

写了接近两年的 Flutter ,发现数据与事件的传递是新手在学习时经常问的问题: 有很多初学者错误的在非常早期就引入 provider BLOC   等模式去管理数据,过量使用外部框架,造成项目混乱难以组织代码。其主要的原因就是因为忽视了基础的,最简单的数据传递方式


很难想象有人把全部数据放在一个顶层 provider 里,然后绝对不写 StatefulWidget。这种项目反正我是不维护,谁爱看谁看。


本文会列举基本的事件与方法传递方式,并且举例子讲明如何使用基础的方式实现这些功能。 本文的例子都基于 flutter 默认的加法 demo 修改,在 dartpad 或者新建 flutter 项目中即可运行本项目的代码例子。



在局部传递数据与事件


先来看下基本的几个应用情况,只要实现了这些情况,在局部就可以非常流畅的传递数据与事件:


注意思考: 下文的 Widget,哪些是 StatefulWidget


描述: 一个 Widget 收到事件后,改变 child 显示的值
实现功能: 点击加号让数字 +1
难度: ⭐

描述: 一个 Widget 在 child 收到事件时,改变自己的值
实现功能: 点击改变页面颜色
难度: ⭐

描述一个 Widget 在 child 收到事件时,触发自己的 state 的方法
实现功能点击发起网络请求,刷新当前页面
难度

描述一个 Widget 自己改变自己的值
实现功能倒计时,从网络加载数据
难度⭐⭐⭐

描述一个 Widget 自己的数据变化时,触发 state 的方法
实现功能一个在数据改变时播放过渡动画的组件
难度⭐⭐⭐⭐

描述一个 Widget 收到事件后,触发 child   的  state 的方法
实现功能点击按钮让一个 child 开始倒计时或者发送请求
难度⭐⭐⭐⭐⭐

我们平时写项目基本也就是上面这些需求了,只要学会实现这些事件与数据传递,就可以轻松写出任何项目了。

使用回调传递事件

使用简单的回调就可以实现这几个需求,这也是整个 flutter 的基础: 如何改变一个 state 内的数据,以及如何改变一个 widget 的数据。


描述: 一个  widget 收到事件后,改变  child 显示的值 
实现功能: 点击加号让数字 +1 

描述: 一个  widget 在  child 收到事件时,改变自己的值 
实现功能: 点击改变页面颜色 

描述: 一个 widgetchild 收到事件时,触发自己的 state 的方法 
实现功能: 点击发起网络请求,刷新当前页面

这几个都是毫无难度的,我们直接看同一段代码就行了

代码:

/// 这段代码是使用官方的代码修改的,通常情况下,只需要使用回调就能获取点击事件class MyHomePage extends StatefulWidget {  MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override _MyHomePageState createState() => _MyHomePageState();}
class _MyHomePageState extends State<MyHomePage> { int _counter = 0;
void _incrementCounter() {    // 在按钮的回调中,您可以设置数据与调用方法 // 在这里,让计数器+1后刷新页面 setState(å() { _counter++; }); }
// setState后就会使用新的数据重新进行build // flutter的build性能非常强,甚至支持每秒60次rebuild // 所以不必过于担心触发build,但是要偶尔注意超大范围的build @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ), floatingActionButton: _AddButton( onAdd: _incrementCounter, ), ); }}
/// 一般会使用GestureDetector来获取点击事件/// 因为官方的FloatingActionButton会自带样式,一般我们会自己写按钮样式class _AddButton extends StatelessWidget { final Function onAdd;
const _AddButton({Key key, this.onAdd}) : super(key: key); @override Widget build(BuildContext context) { return FloatingActionButton( onPressed: onAdd, child: Icon(Icons.add), ); }}


这种方式十分的简单,只需要在回调中改变数据,再 setState 就会触发 build 方法,根据当前的数据重新 build 当前 widget ,这也是 flutter 最基本的刷新方法。


在 State 中改变数据

flutter 中,只有 StatefulWidget 才具有 state state 才具有传统意义上的生命周期 (而不是页面),通过这些周期,可以做到一进入页面,就开始从服务器加载数据,也可以让一个 Widget 自动播放动画

我们先看这个需求:

描述: 一个 Widget 自己改变自己的值 

实现功能: 倒计时,从网络加载数据


这也是一个常见的需求,但是很多新手写到这里就不会写了,可能会错误的去使用 FutureBuilder 进行网络请求,会造成每次都反复请求,实际上这里是必须使用 StatefulWidgetstate 来储存请求返回信息的。


一般项目中,动画,倒计时,异步请求此类功能需要使用 state,其他大多数的功能并不需要存在 state


例如这个 widget,会显示一个数字:

class _CounterText extends StatelessWidget {  final int count;
const _CounterText({Key key, this.count}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Text('$count'), ); }}

可以试着让 widget 从服务器加载这个数字:

class _CounterText extends StatefulWidget {  const _CounterText({Key key}) : super(key: key);
@override __CounterTextState createState() => __CounterTextState();}
class __CounterTextState extends State<_CounterText> { @override void initState() { // 在initState中发出请求 _fetchData(); super.initState(); }
// 在数据加载之前,显示0 int count = 0;
// 加载数据,模拟一个异步,请求后刷新 Future<void> _fetchData() async { await Future.delayed(Duration(seconds: 1)); setState(() { count = 10; }); }
@override Widget build(BuildContext context) { return Center( child: Text('$count'), ); }}


又或者,我们想让这个数字每秒都减 1,最小到 0。那么只需要把他变成 stateful 后,在 initState 中初始化一个 timer,让数字减小:

class _CounterText extends StatefulWidget {  final int initCount;
const _CounterText({Key key, this.initCount:10}) : super(key: key);
@override __CounterTextState createState() => __CounterTextState();}
class __CounterTextState extends State<_CounterText> { Timer _timer;
int count = 0;
@override void initState() { count = widget.initCount; _timer = Timer.periodic( Duration(seconds: 1), (timer) { if (count > 0) { setState(() { count--; }); } }, ); super.initState(); }
@override void dispose() { _timer?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { return Center( child: Text('${widget.initCount}'), ); }}


这样我们就能看到这个 widget   从输入的数字每秒减少 1。

由此可见, widget 可以在  state 中改变数据,这样我们在使用 StatefulWidget 时,只需要给其初始数据, widget 会根据生命周期加载或改变数据。

在这里,我建议的用法是在 Scaffold 中加载数据,每个页面都由一个 Stateful Scaffold 和若干 StatelessWidget 组成,由 Scaffold State 管理所有数据,再刷新即可。

注意,即使这个页面的 body 是 ListView ,也不推荐 ListView 管理自己的 state ,在当前 state 维护数据的 list 即可。使用 ListView.builder 构建列表即可避免更新数组时,在页面上刷新列表的全部元素,保持高性能刷新。

在 State 中监听 widget 变化
描述: 一个  Widget 自己的数据变化时,触发  state 的方法 
实现功能: 一个在数据改变时播放过渡动画的组件


做这个之前,我们先看一个简单的需求: 一行 widget ,接受一个数字,数字是偶数时,距离左边 24px ,奇数时距离左边 60px

这个肯定很简单,我们直接 StatelessWidget 就写出来了;
class _Row extends StatelessWidget {  final int number;
const _Row({ Key key, this.number, }) : super(key: key);
double get leftPadding => number % 2 == 1 ? 60.0 : 24.0;
@override Widget build(BuildContext context) { return Container( height: 60, width: double.infinity, alignment: Alignment.centerLeft, padding: EdgeInsets.only( left: leftPadding, ), child: Text('$number'), ); }}


这样就简单的实现了这个效果,但是实际运行的时候发现,数字左右横跳,很不美观。看来就有必要优化这个 widget ,让他左右移动的时候播放动画,移动过去,而不是跳来跳去。

一个比较简单的方案是,传入一个 AnimationController 来精确控制,但是这样太复杂了。这种场景下,我们在使用的时候通常只想更新数字,再 setState,就希望他在内部播放动画 (通常是过渡动画),就可以不用去操作复杂的 AnimationController 了。

实际上,这个时候我们使用 didUpdateWidget 这个生命周期就可以了,在 state 所依附的 widget   更新时,就会触发这个回调,您可以在这里响应上层传递的数据的更新,在内部播放动画。

代码:
class _Row extends StatefulWidget {  final int number;
const _Row({ Key key, this.number, }) : super(key: key);
@override __RowState createState() => __RowState();}
class __RowState extends State<_Row> with TickerProviderStateMixin { AnimationController animationController;
double get leftPadding => widget.number % 2 == 1 ? 60.0 : 24.0;
@override void initState() { animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 500), lowerBound: 24, upperBound: 60, ); animationController.addListener(() { setState(() {}); }); super.initState(); }
// widget更新,就会触发这个方法 @override void didUpdateWidget(_Row oldWidget) { // 播放动画去当前位置 animationController.animateTo(leftPadding); super.didUpdateWidget(oldWidget); }
@override void dispose() { animationController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return Container( height: 60, width: double.infinity, alignment: Alignment.centerLeft, padding: EdgeInsets.only( left: animationController.value, ), child: Text('${widget.number}'), ); }}


这样在状态之间就完成了一个非常平滑的动画切换,再也不会左右横跳了。

传递 ValueNotifier/自定义 Controller
这里我们还是先看需求


描述: 一个  Widget 收到事件后,触发  child 的  state 的方法 
实现功能: 点击按钮让一个  child 开始倒计时或者发送请求 (调用state的方法) 
难度: ⭐⭐⭐⭐⭐


首先必须明确的是,如果出现在业务逻辑里,这里是显然不合理,是需要避免的。 StatefulWidget 嵌套时应当避免互相调用方法,在这种时候,最好是将 childstate 中的方法与数据,向上提取放到当前层 state 中。


这里可以简单分析一下:

  1. 有数据变化
    有数据变化时,使用 StatedidUpdateWidget 生命周期更加合理。这里我们也可以勉强实现一下,在 flutter 框架中,我推荐使用 ValueNotifier 进行传递,child 监听 ValueNotifier 即可。

  2. 没有数据变化

    没有数据变化就比较麻烦了,我们需要一个 controller 进去,然后 child 注册一个回调进 controller,这样就可以通过 controller 控制。


这里也可以使用 providereventbus 等库,或者用 keyglobalKey 相关方法实现。但是,必须再强调一次: 不管用什么方式实现,这种嵌套是不合理的,项目中需要互相调用 state 的方法时,应当合并写在一个 state 里。原则上,需要避免此种嵌套,无论如何实现,都不应当是项目中的通用做法。

虽然不推荐在业务代码中这样写,但是在框架的代码中是可以写这种结构的 (因为必须暴露接口)。这种情况可以参考 ScrollController ,您可以通过这个 Controller  制滑动状态。

值得一提的是: ScrollController 继承自 ValueNotifier 。所以使用 ValueNotifier 仍然是推荐做法。

其实 controller 模式也是 flutter   源码中常见的模式,一般用于对外暴露封装的方法。 controller   相比于其他的方法,比较复杂,好在我们不会经常用到。

作为例子,让我们实现一个 CountController 类,来帮我们调用组件内部的方法。

代码:
class CountController extends ValueNotifier<int> {  CountController(int value) : super(value);
// 逐个增加到目标数字 Future<void> countTo(int target) async { int delta = target - value; for (var i = 0; i < delta.abs(); i++) { await Future.delayed(Duration(milliseconds: 1000 ~/ delta.abs())); this.value += delta ~/ delta.abs(); } }
// 实在想不出什么例子了,总之是可以这样调用方法 void customFunction() { _onCustomFunctionCall?.call(); }
// 目标state注册这个方法 Function _onCustomFunctionCall;}
class _Row extends StatefulWidget { final CountController controller; const _Row({ Key key, @required this.controller, }) : super(key: key);
@override __RowState createState() => __RowState();}
class __RowState extends State<_Row> with TickerProviderStateMixin { @override void initState() { widget.controller.addListener(() { setState(() {}); }); widget.controller._onCustomFunctionCall = () { print('响应方法调用'); }; super.initState(); }
// 这里controller应该是在外面dispose // @override // void dispose() { // widget.controller.dispose(); // super.dispose(); // }
@override Widget build(BuildContext context) { return Container( height: 60, width: double.infinity, alignment: Alignment.centerLeft, padding: EdgeInsets.only( left: 24, ), child: Text('${widget.controller.value}'), ); }}


使用 controller 可以完全控制下一层 state 的数据和方法调用,比较灵活。但是代码量大,业务中应当避免写这种模式,只在复杂的地方构建 controller 来控制数据。如果您写了很多自定义 controller ,那应该反思您的项目结构是不是出了问题。无论如何实现,这种传递方式都不应当是项目中的通用做法。



单例管理全局数据与事件


全局的数据,可以使用顶层 provider 或者单例管理,我的习惯是使用单例,这样获取数据可以不依赖 context


简单的单例写法,扩展任何属性到单例即可。

class Manager {  // 工厂模式  factory Manager() =>_getInstance();  static Manager get instance => _getInstance();  static Manager _instance;  Manager._internal() {    // 初始化  }  static Manager _getInstance() {    if (_instance == null) {      _instance = new Manager._internal();    }    return _instance;  }}


总结


写这篇文章的原因,是因为看到不少人在学习 flutter 时,对于数据与事件的传递非常的不熟悉,又很早的去学习 provider 等第三方框架,对于基础的东西又一知半解,导致代码混乱项目混乱,不知如何传递数据,如何去刷新界面。所以写这篇文章总结了最基础的各种事件与数据的传递方法。


简单总结, flutter 改变数据最基础的就是这么几种模式:
  • 改变自己 state 的数据, setState child 传递新数据
  • 接受 child 的事件回调
  • child 更新目标数据, child 监听数据的变化,更加细节的改变自己的 state
  • child 传递 controller ,全面控制 child   state

项目中只需要这几种模式就能很简单的全部写完了,使用 provider 等其他的库,代码上并不会有特别大的改善和进步。还是希望大家学习 flutter 的时候,能先摸清基本的写法,再进行更深层次的学习。




长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。



 

 点击屏末 |  | 即刻报名参与 "开发者说·DTalk" 

 



登录查看更多
4

相关内容

【2020新书】使用Kubernetes开发高级平台,519页pdf
专知会员服务
66+阅读 · 2020年9月19日
事件知识图谱构建技术与应用综述
专知会员服务
148+阅读 · 2020年8月6日
【2020新书】高级Python编程,620页pdf
专知会员服务
235+阅读 · 2020年7月31日
专知会员服务
163+阅读 · 2020年7月27日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
重磅!Flutter 2019年最新进展和未来展望
前端之巅
4+阅读 · 2019年6月20日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
UML建模更好的表达产品逻辑
互联网er的早读课
4+阅读 · 2019年5月9日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
基于Linux连接器的审计进程事件实现方案
FreeBuf
9+阅读 · 2019年3月4日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
Tplmap - 扫描服务器端模板注入漏洞的开源工具
黑白之道
6+阅读 · 2018年9月11日
Python | Jupyter导出PDF,自定义脚本告别G安装包
程序人生
7+阅读 · 2018年7月17日
Arxiv
0+阅读 · 2020年12月3日
Arxiv
0+阅读 · 2020年12月3日
The Evolved Transformer
Arxiv
5+阅读 · 2019年1月30日
Arxiv
4+阅读 · 2018年10月31日
Arxiv
9+阅读 · 2018年10月24日
Arxiv
136+阅读 · 2018年10月8日
VIP会员
相关VIP内容
【2020新书】使用Kubernetes开发高级平台,519页pdf
专知会员服务
66+阅读 · 2020年9月19日
事件知识图谱构建技术与应用综述
专知会员服务
148+阅读 · 2020年8月6日
【2020新书】高级Python编程,620页pdf
专知会员服务
235+阅读 · 2020年7月31日
专知会员服务
163+阅读 · 2020年7月27日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
相关资讯
重磅!Flutter 2019年最新进展和未来展望
前端之巅
4+阅读 · 2019年6月20日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
UML建模更好的表达产品逻辑
互联网er的早读课
4+阅读 · 2019年5月9日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
基于Linux连接器的审计进程事件实现方案
FreeBuf
9+阅读 · 2019年3月4日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
Tplmap - 扫描服务器端模板注入漏洞的开源工具
黑白之道
6+阅读 · 2018年9月11日
Python | Jupyter导出PDF,自定义脚本告别G安装包
程序人生
7+阅读 · 2018年7月17日
相关论文
Arxiv
0+阅读 · 2020年12月3日
Arxiv
0+阅读 · 2020年12月3日
The Evolved Transformer
Arxiv
5+阅读 · 2019年1月30日
Arxiv
4+阅读 · 2018年10月31日
Arxiv
9+阅读 · 2018年10月24日
Arxiv
136+阅读 · 2018年10月8日
Top
微信扫码咨询专知VIP会员