本文原作者: 马嘉伦,原文发布于 segmentfault
https://segmentfault.com/a/1190000023338363
本文使用原生 Flutter 形式设计代码,只讲最基础的东西,不使用任何其他第三方库 ( Provider 等)
很难想象有人把全部数据放在一个顶层 provider 里,然后绝对不写 StatefulWidget。这种项目反正我是不维护,谁爱看谁看。
在局部传递数据与事件
注意思考: 下文的 Widget,哪些是 StatefulWidget?
使用回调传递事件
使用简单的回调就可以实现这几个需求,这也是整个 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;_MyHomePageState createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> {int _counter = 0;void _incrementCounter() {// 在按钮的回调中,您可以设置数据与调用方法// 在这里,让计数器+1后刷新页面setState(å() {_counter++;});}// setState后就会使用新的数据重新进行build// flutter的build性能非常强,甚至支持每秒60次rebuild// 所以不必过于担心触发build,但是要偶尔注意超大范围的buildWidget 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);Widget build(BuildContext context) {return FloatingActionButton(onPressed: onAdd,child: Icon(Icons.add),);}}
在 State 中改变数据
描述: 一个 Widget 自己改变自己的值
实现功能: 倒计时,从网络加载数据
这也是一个常见的需求,但是很多新手写到这里就不会写了,可能会错误的去使用 FutureBuilder 进行网络请求,会造成每次都反复请求,实际上这里是必须使用 StatefulWidget 的 state 来储存请求返回信息的。
一般项目中,动画,倒计时,异步请求此类功能需要使用 state,其他大多数的功能并不需要存在 state。
例如这个 widget,会显示一个数字:
class _CounterText extends StatelessWidget {final int count;const _CounterText({Key key, this.count}) : super(key: key);Widget build(BuildContext context) {return Center(child: Text('$count'),);}}
可以试着让 widget 从服务器加载这个数字:
class _CounterText extends StatefulWidget {const _CounterText({Key key}) : super(key: key);__CounterTextState createState() => __CounterTextState();}class __CounterTextState extends State<_CounterText> {void initState() {// 在initState中发出请求_fetchData();super.initState();}// 在数据加载之前,显示0int count = 0;// 加载数据,模拟一个异步,请求后刷新Future<void> _fetchData() async {await Future.delayed(Duration(seconds: 1));setState(() {count = 10;});}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);__CounterTextState createState() => __CounterTextState();}class __CounterTextState extends State<_CounterText> {Timer _timer;int count = 0;void initState() {count = widget.initCount;_timer = Timer.periodic(Duration(seconds: 1),(timer) {if (count > 0) {setState(() {count--;});}},);super.initState();}void dispose() {_timer?.cancel();super.dispose();}Widget build(BuildContext context) {return Center(child: Text('${widget.initCount}'),);}}
描述: 一个 Widget 自己的数据变化时,触发 state 的方法 实现功能: 一个在数据改变时播放过渡动画的组件
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;Widget build(BuildContext context) {return Container(height: 60,width: double.infinity,alignment: Alignment.centerLeft,padding: EdgeInsets.only(left: leftPadding,),child: Text('$number'),);}}
class _Row extends StatefulWidget {final int number;const _Row({Key key,this.number,}) : super(key: key);__RowState createState() => __RowState();}class __RowState extends State<_Row> with TickerProviderStateMixin {AnimationController animationController;double get leftPadding => widget.number % 2 == 1 ? 60.0 : 24.0;void initState() {animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 500),lowerBound: 24,upperBound: 60,);animationController.addListener(() {setState(() {});});super.initState();}// widget更新,就会触发这个方法void didUpdateWidget(_Row oldWidget) {// 播放动画去当前位置animationController.animateTo(leftPadding);super.didUpdateWidget(oldWidget);}void dispose() {animationController.dispose();super.dispose();}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 里。原则上,需要避免此种嵌套,无论如何实现,都不应当是项目中的通用做法。
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);__RowState createState() => __RowState();}class __RowState extends State<_Row> with TickerProviderStateMixin {void initState() {widget.controller.addListener(() {setState(() {});});widget.controller._onCustomFunctionCall = () {print('响应方法调用');};super.initState();}// 这里controller应该是在外面dispose// @override// void dispose() {// widget.controller.dispose();// super.dispose();// }Widget build(BuildContext context) {return Container(height: 60,width: double.infinity,alignment: Alignment.centerLeft,padding: EdgeInsets.only(left: 24,),child: Text('${widget.controller.value}'),);}}
单例管理全局数据与事件
全局的数据,可以使用顶层 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 等第三方框架,对于基础的东西又一知半解,导致代码混乱项目混乱,不知如何传递数据,如何去刷新界面。所以写这篇文章总结了最基础的各种事件与数据的传递方法。
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"