头图

哈喽,我是老刘

前段时间有人问我这个问题碰到没有:
Flutter - 升级3.19之后页面多次rebuild? - 技术栈 (jishuzhan.net)
说实话我们没有碰到这个问题

我先来简单解释一下这个问题,本质上是因为使用了 InheritedWidget

通过InheritedWidget向子树传递数据

InheritedWidget可以向其子树中的所有Widget提供数据。这使得无关的Widget能方便地获取同一个InheritedWidget提供的数据,实现Widget树中不直接相关Widget之间的数据共享。
Flutter SDK 中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale(当前语言环境)信息的。
其使用方法如下
实现一个InheritedWidget

class MyInheritedWidget extends InheritedWidget {  // 继承InheritedWidget
  final int data;
MyInheritedWidget({required this.data, required Widget child}) : super(child: child);
@override
  // 这个方法定义了当数据发送变化时是否通知子树中的子Widget。
  // 它返回一个布尔值,true表示通知子Widget,false表示不通知。
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.data != data;
  }
  // 子Widget可以通过调用MyInheritedWidget.of()静态方法来获取MyInheritedWidget实例,并获取其提供的数据。
  static MyInheritedWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()!;
  }
}

获取InheritedWidget中的数据

class MyText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      '${MyInheritedWidget.of(context).data}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

当InheritedWidget中的数据发生变化时,就会通知所有通过InheritedWidget.of()方法注册了关注数据变化的成员。
这时这些子树中的组件就会重绘。

其实前面文章中的问题就是当你使用ModalRoute.of(context)方法获取页面路由的参数时,其实也就是向一个全局级别的InheritedWidget节点注册了关注其变化。
而当这个全局的路由节点的行为发生变化后(页面退栈通知数据变化),就会出现原先没有的重绘现象出现了。

为什么我们的代码没有这个问题

我们在传递页面参数时其实是没有使用ModalRoute.of(context)的方式获取页面参数的。
我们使用的是页面类的构造参数。
举个例子,比如打开一个商品详情页,需要传递商品id作为页面参数。
代码如下

class ProductDetailPage extends StatefulWidget {
  final String productId;

  const ProductDetailPage({required this.productId});

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

class _ProductDetailPageState extends State<ProductDetailPage> {
  @override
  void initState() {
    super.initState();
    // 使用productId获取商品详情
  }

  @override
  Widget build(BuildContext context) {
    // 根据productId构建UI
    return Scaffold(
      // ...
    );
  }
}

定义路由可以使用动态生成路由的方式:

MaterialApp(
  onGenerateRoute: (RouteSettings settings) {
    // 通过settings.name可以获取传入的路由名称,并据此返回不同的路由
    final productId = settings.arguments['id'] as String;
    return MaterialPageRoute(
      builder: (context) => ProductDetailPage(productId: productId),
    );
  }
)

那为什么我们要选择这种方式传递页面参数,而不是ModalRoute.of(context)的方式呢?
这其实是一种本能,一种下意识的行为。

两个思维习惯

1、减少不可控因素

老刘写了十多年的代码了,光Flutter就写了快6年。
这么多年的实战形成的习惯就是对不可控的外部依赖心怀警惕。
InheritedWidget就是一种很典型的场景。
如果是自己写的InheritedWidget还好,但如果是外部的,比如系统SDK的。
那么你怎么保证它通知的数据变化时你想要的呢?
这次的问题不就是很典型的例子吗。
远隔千里之外的人修改了几行代码,就对你的App的行为造成了影响。

2、模块化思维

也许你觉得写一个页面就是一个页面。
但是在我看来,很有可能某一天它就是某个页面的一个组件。
假设有一天你的产品要适配pad端
那么很有可能商品列表页和商品详情页会合并成一个页面:左边是列表右边是详情。
这时候原先独立的详情页就是页面的一个组件了。
image.png
这时候是不是通过构造参数传递商品id会合理很多?

总结

我们从一个实际的bug出发,解释了为什么建议大家通过构造参数进行页面传参。
进而引出了关于日常编码中的一些很具体的思维习惯。
总之很多时候最简单直接的用法可能也是最好的选择。

好了,如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》


程序员老刘
1 声望2 粉丝

客户端架构师,客户端团队负责人。一个月带领客户端团队从0基础迁移到Flutter 。目前团队已使用Flutter五年。