Flutter入门与实战(四十一): 从InheritedWidget深入了解状态管理机制(上)

1,739 阅读5分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

本文翻译自 Flutter 官方推荐的文章:Managing Flutter Application State With InheritedWidgets。通过官网文档或推荐文章,能够让我们更好地了解 Flutter 的状态管理机制。

前言

通常来说,交互式应用可以分为三个部分:ModelViewController,也就是我们常说的MVC 模式。使用过Flutter样例的人会对使用Widget和回调方式来构建视图和控制器的响应式方式很熟悉。但是,对于 Model 这一层来说,确未必那么清晰。Flutter 的 Model 层实际代表了其保持的状态。Widget 为状态提供了可视化的呈现,并且允许用户修改它。 当widgetbuild方法从 Model 中获取值时,或者回调函数修改 Model 值的时候,widget将会随着 Model 的改变而重新构建。本篇文章就是介绍这一切是怎么发生的。 本篇文章回顾了 Flutter 的有状态组件和 InheritedWidget 类如何将应用的可视化元素绑定到 Model 上。并且引入了一个可以轻松植入应用的ModelBinding类。

声明

本篇介绍的构建 MVC 应用的方式并不是唯一的。如果你要构建大型的应用的话,有很多种方式可以将 Flutter 绑定到模型上。本篇结尾也会列出其中的一些。换言之,即便你决定最后不使用 ModelBinding 类,你也可以收获到 Flutter 的状态管理机制。

这并不是面向初学者的文章,你至少需要对 Flutter 的 API 有一定的了解,例如:

  • 你能够熟练地使用 Dart 编写类,并且了解==操作符和哈希码重载,以及泛型方法。
  • 对基本的 Flutter 组件类熟悉,并且懂得如何自己写一个新的组件。

应用的模型

为了展示本篇的示例,我们需要一个样例应用模型。为了聚焦,我们会将这个模型设计得尽可能地简单。在我们的模型类中只有一个值,以及包括了操作符==hashCode 重载。

class ViewModel {
  const ViewModel({ this.value = 0 });

  final int value;

  @override
  bool operator ==(Object other) {
    if (identical(this, other))
      return true;
    if (other.runtimeType != runtimeType)
      return false;
    final ViewModel otherModel = other;
    return otherModel.value == value;
  }

  @override
  int get hashCode => value.hashCode;

  static ViewModel of(BuildContext context) {
    final ModelBinding binding = context.dependOnInheritedWidgetOfExactType(aspect: ModelBinding);
    return binding.model;
  }
}

当然,这个模型也可以根据应用的实际情况进行扩展。 注意,这是一个不可变的模型,因此如果要改变的只能是替换它。下面的MVC方式也可以用可变的模型,但是那样会稍微有点复杂。

将模型与有状态组件绑定

这是集成模型最简单的方式,对于只需要一个下午就搞定的应用来说非常合适。 有状态组件会与一个保持状态的 State 对象关联。这个 State 对象会的 build 的方法会构建该组件的子组件树,就像无状态组件的build 方法一样。调用State 对象的 setState 方法时,在间隔一个显示桢切换的时间间隔后,将会触发组件重新构建。如果有状态组件的状态对象持有模型,那么用于配置它的build方法在调用 setState 方法时,就会使用模型的值。 下面的这个有状态组件十分简单,只是持有了一个 ViewModel 对象,然后提供了一个 update 方法来更新模型。

class ViewController extends StatefulWidget {
  _ViewControllerState createState() => _ViewControllerState();
}

class _ViewControllerState extends State<ViewController> {
  Model currentModel = ViewModel();
  
  void updateModel(ViewModel newModel) {
    if (newModel != currentModel) {
      setState(() {
        currentModel = newModel;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        updateModel(ViewModel(value: currentModel.value + 1));
      },
      child: Text('Hello World ${currentModel.value}),
    );
  }
}

使用有状态组件绑定的限制

使用上面的方式写代码的肯定是个草包(😂这话不是我说的,误伤了请自醒——我们之前的示例代码也是这么写的,确实很初级)。对于相对大规模的应用来说,就很不适用了。具体来说,会有如下的缺陷:

  • 当模型改变的时候,整个 ViewController 以及整个组件树都会被重建,而不仅仅是依赖模型的那个组件(即 Text)。
  • 如果子组件需要使用模型的值,模型只能通过构造器参数传递,或者使用回调闭包。
  • 如果再往下层级的组件需要使用模型的值,那只能是沿着组件树的链条一层层往下传递模型对象。
  • 如果子组件需要修改模型的值,那就必须调用 ViewController 传递的回调函数。这个例子中就如同 RaisedButtononPressed 方法那样。

因此,有状态组件其实更适用于创建自己内部的状态,而不适用于在复杂应用中共享数据模型。而对应程序员来说,搞定复杂的事情才让我们显得更有价值

版本0:使用 InheritedWidget 绑定模型

InheritedWidget 类有一些特殊的使得它很适合在组件树里共享模型。

  • 给定一个 BuildContext,查找一个特定类型的最近的InheritedWidget 的祖先节点十分便捷,只需要按表查找就行。
  • InheritedWidget 会跟踪他们的依赖,例如用于访问InheritedWidgetBuildContext。当一个 InheritedWidget 重建时,所有它依赖的对象都会被重建。

实际上你很可能已经接触过InheritedWidget 了,例如 Theme 这个组件。Theme.of(context)方法会返回 ThemeThemeData 对象,并且将 context 当做是Theme的一个依赖对象。如果 Theme 对象被重建,并且使用了不同的 ThemeData 值,那么所有依赖于 Theme.of()的组件都会被自动重建。

使用自定义的 InheritedWidget 子类可以按相同的方式实现应用模型的宿主。这里,我们把这个子类称之为 ModelBinding,因为它将应用的组件和模型关联在一起了。

class ModelBinding extends Inherited {
  ModelBinding({
    Key key,
    this.model = const ViewModel(),
    Widget child,
  }): assert(model != null), super(Key: key, child:child);
  
  final ViewModel model;
  
  @override
  bool updateShouldNotify(ModelBinding oldWidget) => model != oldWidget.model;
}

updateShouldNotify 方法在 ModelBinding 被重建时会被调用。如果返回值是 true,那么依赖它的全部组件都会被重建。

BuildContext inheritFromWidgetOfExactType()方法用于查找一个 InheritedWidget 。由于这个方式有点丑,我们稍后再来介绍它。通常,这个方法是使用静态方法包裹。将查找方法加入到ViewModel能够使得任何依赖 ModelBinding 对象的下级组件都可以通过 Model.of(context)方法获取到 ViewModel 对象。

// 现在在 ModelBinding 的下级组件可以通过 Model.of(context)访问了
Text('Hello WOrld ${ViewModel.of(context).value}')

任何 ModelBinding 的下级都可以这么做,而无需一层层传递 ViewModel 对象了。如果ViewModel 对象发生了改变,下级组件就像 ViewController 一样自动被重建。

ModelBinding 所在的组件自身必须是有状态组件。为了更改 ViewModel 对象,该组件还是需要调用 setState 方法。这里我们使用了一个 StateViewController 有状态组件来持有模型对象,而更新Model 对象的方法被当做回调函数传递给了 ViewController

class StateViewController extends StatefulWidget {
  StateViewController({Key key}) : super(key: key);

  @override
  _StateViewControllerState createState() => _StateViewControllerState();
}

class _StateViewControllerState extends State<StateViewController> {
  ViewModel currentModel = ViewModel();

  void _updateModel(ViewModel newModel) {
    setState(() {
      currentModel = newModel;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('模型绑定版本0'),
      ),
      body: Center(
        child: ModelBinding(
          model: currentModel,
          child: ViewController(_updateModel),
        ),
      ),
    );
  }
}

这种情况下,ViewModel 类是简单的不可变对象,因此只需要赋值一个新的ViewModel对象替换即可完成更新。替换该对象也可能更复杂,例如如果对象引用的 对象需要进行生命周期管理,那么替换该模型的时候可能会需要销毁部分旧的对象。

这个版本的缺陷

运行结果就不贴图了,就是点击按钮数字自动加1,代码已提交至:Flutter 状态管理代码。这个版本从好的方面来看,这个版本的 ModelBinding 类使得组件很容易获取模型对象,并且当模型改变的时候可以自动重建。

但是,反过来,这个版本还需要使用 updateModel 回调方法沿着组件树传递到实际控制状态改变的组件,这种方式的代码并不好维护。下一个版本我们来实现一个更通用的 ModelBinding 类,是的子组件可以直接通过ModelBinding提供的 update 方法更新ViewModel 对象。

总结

本篇介绍了 Flutter 应用中的 MVC 模型,对于 Flutter 而言,应用中模型实际上就是组件的状态。如果直接通过一层层的状态传递去控制组件树的下级组件的显示,将会导致代码耦合严重。因此,本篇引入了一个 ModelBinding 类,通过继承 InheritedWidget来实现子组件可以直接访问上级组件的状态,从而避免了状态参数的 层层传递。当然,这个版本还存在一个缺陷,那就是更改状态的回调方法还是需要沿着组件树传递,这个我们在下篇会改造一个更通用的 ModelBinding 类。


我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章。

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!