Flutter局部刷新技巧

1、为什么需要局部刷新

如下图场景:在一个Navigator的某Router上有个Scffold页面,页面上并列三个StatefulWidget,分别是A、B、C。

此时此页面对应的Tree应为右图所示。

问题:当A节点上显示的某文本需要变化,怎么操作,才是最好的选择呢?

回答:这种场景很多,将A节点的文案对象放入State属性中,修改为新的文本,调用setStates()方法即可。那么怎么才是更高效的需要理解setStates()方法做了哪些。

image.png

2、setStates()做了什么呢?

简单的说就是将setStates的Widget对象对应的Element对象标记为dirty(脏的,意思是需要刷新的),并将其存储到了一个全局的链表中。然后就是等待,等待什么呢?等待系统下一帧的Vsync通知,当系统告知我们下一帧可以显示了,widgetBinding就会找到这个存放着需要刷新element的链表重新绘制。

所以我们暂且理解为:谁(这里理解为某个StatefulWidget实例)调用了setStates(),谁就会执行build()方法,并重新绘制。

所以如果只需要A需要重绘,只需要A调用setStates()即可,那么具体代码我们怎么写呢?

3、具体代码书写

1、错误示范:(整个页面刷新)

 String a_test;
  String b_test;
  String c_test;

  @override
  void initState() {
    super.initState();
    a_test = 'A';
    b_test = 'B';
    c_test = 'C';
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text(a_test),
        Text(b_test),
        Text(c_test),
        GestureDetector(
          onTap: () {
            a_test = 'A_NEW';
            setState(() {});
          },
          child: Text('点击修改A的文案'),
        )
      ],
    );
  }

这种情况会导致整个页面的build()被执行,那么Column以及Column中的四个child都会重新创建,如果是复杂页面,将会出现卡帧,这无疑违背高效原则。

2、一个比较实在的正确写法:

既然只需要A刷新,那么我们把A单独抽成一个类并集成于StatefulWidget,我们只在A类中做setStates():

父节点:

 @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        _AText(
          key: aKey,
        ),
        Text(b_test),
        Text(c_test),
        GestureDetector(
          onTap: () {
            __ATextState astate = aKey.currentState;
            astate.updateText();
          },
          child: Text('点击修改A的文案'),
        )
      ],
    );
  }

A节点:

 String a_test;
  @override
  void initState() {
    super.initState();
    a_test = 'A';
  }

  @override
  Widget build(BuildContext context) {
    return Text(a_test);
  }

  void updateText() {
    a_test = 'A_NEW';
    setState(() {});
  }

这种写法,经过测试可以达到只有A在build,但是书写臃肿,违背优雅原则。

3、利用通知的方式

1、ValueNotifier的方式:
@override
  void initState() {
    super.initState();
    a_test = 'A';
    b_test = 'B';
    c_test = 'C';
    a_value_noti = ValueNotifier<String>(a_test);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ValueListenableBuilder(
            valueListenable: a_value_noti,
            builder: (BuildContext context, String value, Widget child) {
              return Text(value);
            }),
        Text(b_test),
        Text(c_test),
        GestureDetector(
          onTap: () {
            a_value_noti.value = 'A_NEW';
          },
          child: Text('点击修改A的文案'),
        )
      ],
    );
  }
2、Stream的方式
  String a_test;
  String b_test;
  String c_test;
  StreamController<String> aStreamC;
  @override
  void initState() {
    super.initState();
    a_test = 'A';
    b_test = 'B';
    c_test = 'C';
    aStreamC = StreamController<String>();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        StreamBuilder(
            stream: aStreamC.stream,
            initialData: a_test,
            builder: (context, AsyncSnapshot snapshot) {
              return Text(snapshot.data);
            }),
        Text(b_test),
        Text(c_test),
        GestureDetector(
          onTap: () {
            aStreamC.add('A_NEW');
          },
          child: Text('点击修改A的文案'),
        )
      ],
    );
  }

4、数据共享的方式

1、使用InHeritedWidget:

InHeritedWidget的使用固定,可用于共享state,如下面的使用:

  • 创建自己的InHeritedWidget类:
class MyInheritedWidget extends InheritedWidget {
  final String aText;

  MyInheritedWidget(this.aText, Widget child) : super(child: child);

  static MyInheritedWidget of(BuildContext context, {bool rebuild = true}) {
    if (rebuild) {
      return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
    }
    return context.findAncestorWidgetOfExactType<MyInheritedWidget>();
  }

  @override
  bool updateShouldNotify(MyInheritedWidget old) {
    return aText != old.aText;
  }
}
  • 在需要使用到共享数据的父试图层,用我们创建的InHeritedWidget包裹:
return MyInheritedWidget(
        a_test,
        Column(
          children: <Widget>[
            _AText(),
            _AText(),
            Text(c_test),
            GestureDetector(
              onTap: () {
                setState(() {
                  this.a_test = 'bbb';
                });
              },
              child: Text('点击修改A的文案'),
            )
          ],
        ));
  • 使用到共享数据的widget,可以使用InHeritedWidget.of(context)取得对应属性:
Widget build(BuildContext context) {
    return Text(MyInheritedWidget.of(context, rebuild: false).aText);
  }
2、使用Provider

Provider其实就是对InHeritedWidget的封装,用法固定,感兴趣的同学可以自行查找,本篇不做介绍。

4、对比以上方式,总结使用场景

1、上述比较实在的写法的总结:

缺点:用到了Key取到state,用法诡异,可阅读性很差。

优点:对复杂页面,我们可以借鉴把child分开定义的方式书写,可以使一个复杂页面分模块管理。

2、通知的方式:

缺点:数据单一,不太方便使用在复杂页面,多数据量的存储,当然我们可以把多个数据包装成一个bean,或者定义多个通知对象,但是这种用法违背我们的初衷。

优点:使用简单,对于基本数据类型的更新方式,有很大的优势。

3、使用共享数据的方式:

缺点:框架量级较大,尤其是provider,虽然使用方式比较固定,但是也没有通知的方式那么便捷。

优点:可实现多级页面或单级多层次页面的数据共享,使用后代码结构清晰,阅读性和可扩展性强。

5、补充知识点,RepaintBoundary的使用:

为什么使用RepaintBoundary?

上面我们说到setStates只作用于setStates的调用方及其子视图。但我在renderObject --> markneedsPaint方法中发现,调用方的父试图虽然未执行build方法做重绘,但是系统却在遍历比较父试图是否需要build:

void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) { // 如果这个属性为ture,则不会继续递归执行父试图是否需要刷新的方法
      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {// 如果isRepaintBoundary为false,就是寻找其父试图执行markNeedsPaint方法,会一直递归到Router。
      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
      assert(parent == this.parent);
    } else {
      if (owner != null)
        owner.requestVisualUpdate();
    }
  }

上述我们得到结论,在子视图是独立展示且绝对不会影响到父试图的场景下(如屏幕上的stack,stack中Position的变化时,下层页面其实无需变化),我们可以将这个子视图用RepaintBoundary包裹起来。

这样则是最优雅和高效的写法。

点赞、关注、评论三连走一波。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容