本文使用原生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
收到事件时,改变自己的值
实现功能:点击改变页面颜色描述:一个
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
最基本的刷新方法。
在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
构建列表即可避免更新数组时,在页面上刷新列表的全部元素,保持高性能刷新。
描述:一个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}'), ); } }
这样在状态之间就完成了一个非常平滑的动画切换,再也不会左右横跳了。
这里我们还是先看需求
描述:一个Widget
收到事件后,触发child
的state
的方法
实现功能:点击按钮让一个child
开始倒计时或者发送请求(调用state的方法)
难度:⭐⭐⭐⭐⭐
首先必须明确的是,如果出现在业务逻辑里,这里是显然不合理,是需要避免的。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
的时候,能先摸清基本的写法,再进行更深层次的学习。