【开发经验】Flutter组件的事件传递与数据控制
source link: https://juejin.im/post/5f1818245188252e7433aeb4
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
本文使用原生 Flutter
形式设计代码,只讲最基础的东西,不使用任何其他第三方库( Provider
等)
写了接近两年的 Flutter
,发现数据与事件的传递是新手在学习时经常问的问题:有很多初学者错误的在非常早期就引入 provider
, BLOC
等模式去管理数据, 过量使用外部框架
,造成项目混乱难以组织代码。其主要的原因就是因为 忽视了基础的,最简单数据传递方式
。
很难想象有人把全部数据放在一个顶层 provider
里,然后绝对不写 StatefulWidget
。这种项目反正我是不维护,谁爱看谁看。
本文会列举基本的事件与方法传递方式,并且举例子讲明如何使用基础的方式实现这些功能。本文的例子都基于 flutter
默认的加法demo修改,在 dartpad
或者新建 flutter
项目中即可运行本项目的代码例子。
在局部传递数据与事件
先来看下基本的几个应用情况,只要实现了这些情况,在局部就可以非常流畅的传递数据与事件:
注意思考:下文的 Widget
,哪些是 StatefulWidget
?
描述:一个 Widget
收到事件后,改变child显示的值
实现功能:点击加号让数字+1
难度::star:
描述:一个 Widget
在child收到事件时,改变自己的值
实现功能:点击改变页面颜色
难度::star:
描述:一个 Widget
在child收到事件时,触发自己的state的方法
实现功能:点击发起网络请求,刷新当前页面
难度::star:
描述:一个 Widget
自己改变自己的值
实现功能:倒计时,从网络加载数据
难度::star::star::star:
描述:一个 Widget
自己的数据变化时,触发 state
的方法
实现功能:一个在数据改变时播放过渡动画的组件
难度::star::star::star::star:
描述:一个 Widget
收到事件后,触发 child
的 state
的方法
实现功能:点击按钮让一个 child
开始倒计时或者发送请求
难度::star::star::star::star::star:
我们平时写项目基本也就是上面这些需求了,只要学会实现这些事件与数据传递,就可以轻松写出任何项目了。
使用回调传递事件
使用简单的回调就可以实现这几个需求,这也是整个 flutter
的基础:如何改变一个 state
内的数据,以及如何改变一个 widget
的数据。
描述:一个 widget
收到事件后,改变 child
显示的值
实现功能:点击加号让数字+1
描述:一个 widget
在 child
收到事件时,改变自己的值
实现功能:点击改变页面颜色
描述:一个 widget
在 child
收到事件时,触发自己的 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
进行网络请求,会造成每次都反复请求,实际上这里是必须使用 StatefulWidget
的 state
来储存请求返回信息的。
一般项目中,动画,倒计时,异步请求此类功能需要使用 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}'), ); } } 复制代码
这样在状态之间就完成了一个非常平滑的动画切换,再也不会左右横跳了。
方法3: 传递ValueNotifier/自定义Controller
这里我们还是先看需求
描述:一个 Widget
收到事件后,触发 child
的 state
的方法
实现功能:点击按钮让一个 child
开始倒计时或者发送请求(调用state的方法)
难度::star::star::star::star::star:
首先必须明确的是,如果出现在业务逻辑里,这里是 显然不合理
,是需要避免的。 StatefulWidget
嵌套时 应当避免互相调用方法
,在这种时候,最好是将 child
的 state
中的方法与数据,向上提取放到当前层 state
中。
这里可以简单分析一下:
-
有数据变化
有数据变化时,使用
State
的didUpdateWidget
生命周期更加合理。这里我们也可以勉强实现一下,在flutter
框架中,我推荐使用ValueNotifier
进行传递,child
监听ValueNotifier
即可。 -
没有数据变化 没有数据变化就比较麻烦了,我们需要一个
controller
进去,然后child
注册一个回调进controller
,这样就可以通过controller
控制。
这里也可以使用 provider
, eventbus
等库,或者用 key
, globalKey
相关方法实现。但是,必须再强调一次:不管用什么方式实现,这种嵌套是不合理的,项目中需要互相调用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; } } 复制代码
总结
作者:马嘉伦 日期:2020/07/22 平台:Segmentfault,掘金社区,勿转载
我的其他文章: 【开发经验】Flutter避免代码嵌套,写好build方法 【Flutter工具】fmaker:自动生成倍率切图/自动更换App图标 【Flutter应用】Flutter精仿抖音开源 【Flutter工具】可能是Flutter上最简单的本地数据保存方案
写这篇文章的原因,是因为看到不少人在学习 flutter
时,对于数据与事件的传递非常的不熟悉,又很早的去学习 provider
等第三方框架,对于基础的东西又一知半解,导致代码混乱项目混乱,不知如何传递数据,如何去刷新界面。所以写这篇文章总结了最基础的各种事件与数据的传递方法。
简单总结, flutter
改变数据最基础的就是这么几种模式:
-
改变自己
state
的数据,setState
向child
传递新数据 -
接受
child
的事件回调 -
向
child
更新目标数据,child
监听数据的变化,更加细节的改变自己的state
-
向
child
传递controller
,全面控制child
的state
项目中只需要这几种模式就能很简单的全部写完了,使用 provider
等其他的库,代码上并不会有特别大的改善和进步。还是希望大家学习 flutter
的时候,能先摸清基本的写法,再进行更深层次的学习。
很遗憾的说,推酷将在这个月底关闭。人生海海,几度秋凉,感谢那些有你的时光。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK