关于前端:开发经验Flutter组件的事件传递与数据控制

8次阅读

共计 8938 个字符,预计需要花费 23 分钟才能阅读完成。

本文应用原生 Flutter 模式设计代码,只讲最根底的货色,不应用任何其余第三方库(Provider等)

写了靠近两年的 Flutter,发现数据与事件的传递是老手在学习时常常问的问题:有很多初学者谬误的在十分晚期就引入providerBLOC 等模式去治理数据,适量应用内部框架 ,造成我的项目凌乱难以组织代码。其次要的起因就是因为 漠视了根底的,最简略的数据传递形式

很难设想有人把全副数据放在一个顶层 provider 里,而后相对不写StatefulWidget。这种我的项目反正我是不保护,谁爱看谁看。

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

在部分传递数据与事件

先来看下根本的几个利用状况,只有实现了这些状况,在部分就能够十分晦涩的传递数据与事件:

留神思考:下文的Widget,哪些是StatefulWidget

形容:一个 Widget 收到事件后,扭转 child 显示的值
实现性能:点击加号让数字 +1
难度:⭐

形容:一个 Widget 在 child 收到事件时,扭转本人的值
实现性能:点击扭转页面色彩
难度:⭐

形容:一个 Widget 在 child 收到事件时,触发本人的 state 的办法
实现性能:点击发动网络申请,刷新以后页面
难度:⭐

形容:一个 Widget 本人扭转本人的值
实现性能:倒计时,从网络加载数据
难度:⭐⭐⭐

形容:一个 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 的时候,能先摸清根本的写法,再进行更深层次的学习。

正文完
 0