【开发经验】Flutter组件的事件传递与数据控制

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

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

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

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

在局部传递数据与事件

先来看下基本的几个应用状况,只要实现了这些状况,在局部就能够很是流畅的传递数据与事件:网络

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

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

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

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

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

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

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

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

使用回调传递事件

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

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

描述:一个widgetchild收到事件时,改变本身的值
实现功能:点击改变页面颜色

描述:一个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才具备statestate才具备传统意义上的生命周期(而不是页面),经过这些周期,能够作到一进入页面,就开始从服务器加载数据,也可让一个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中加载数据,每一个页面都由一个StatefulScaffold和若干StatelessWidget组成,由ScaffoldState管理全部数据,再刷新便可。

注意,即便这个页面的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收到事件后,触发childstate的方法 实现功能:点击按钮让一个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;
  }
}
复制代码

总结

做者:马嘉伦 日期:2020/07/22 平台:Segmentfault,掘金社区,勿转载

个人其余文章: 【开发经验】Flutter避免代码嵌套,写好build方法 【Flutter工具】fmaker:自动生成倍率切图/自动更换App图标 【Flutter应用】Flutter精仿抖音开源 【Flutter工具】多是Flutter上最简单的本地数据保存方案

写这篇文章的缘由,是由于看到很多人在学习flutter时,对于数据与事件的传递很是的不熟悉,又很早的去学习provider等第三方框架,对于基础的东西又只知其一;不知其二,致使代码混乱项目混乱,不知如何传递数据,如何去刷新界面。因此写这篇文章总结了最基础的各类事件与数据的传递方法。

简单总结,flutter改变数据最基础的就是这么几种模式:

  • 改变本身state的数据,setStatechild传递新数据
  • 接受child的事件回调
  • child更新目标数据,child监听数据的变化,更加细节的改变本身的state
  • child传递controller,全面控制childstate

项目中只须要这几种模式就能很简单的所有写完了,使用provider等其余的库,代码上并不会有特别大的改善和进步。仍是但愿你们学习flutter的时候,能先摸清基本的写法,再进行更深层次的学习。

相关文章
相关标签/搜索