1、为什么需要局部刷新如下图场景:在一个Navigator的某Router上有个Scffold页面,页面上并列三个StatefulWidget,分别是A、B、C。 此时此页面对应的Tree应为右图所示。 问题:当A节点上显示的某文本需要变化,怎么操作,才是最好的选择呢? 回答:这种场景很多,将A节点的文案对象放入State属性中,修改为新的文本,调用setStates()方法即可。那么怎么才是更高效的需要理解setStates()方法做了哪些。 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: [ 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: [ _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(a_test); } @override Widget build(BuildContext context) { return Column( children: [ 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 aStreamC; @override void initState() { super.initState(); a_test = 'A'; b_test = 'B'; c_test = 'C'; aStreamC = StreamController(); } @override Widget build(BuildContext context) { return Column( children: [ 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,如下面的使用: 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(); } return context.findAncestorWidgetOfExactType(); } @override bool updateShouldNotify(MyInheritedWidget old) { return aText != old.aText; }}
- 在需要使用到共享数据的父试图层,用我们创建的InHeritedWidget包裹:
return MyInheritedWidget( a_test, Column( children: [ _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、使用ProviderProvider其实就是对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包裹起来。 这样则是最优雅和高效的写法。 点赞、关注、评论三连走一波。 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |