Flutter 如何防止颤振中不必要的渲染?

Flutter 如何防止颤振中不必要的渲染?,flutter,Flutter,下面是一个最小的应用程序,表明了我的担忧。如果您运行它,您将看到每个可见项的构建方法都会运行,即使只有一个项的数据发生了更改 导入“包:flift/foundation.dart”; 进口“包装:颤振/材料.省道”; void main(){ runApp(MyApp()); } 类MyList扩展了无状态小部件{ 最后清单项目; MyList(此.items); @凌驾 小部件构建(构建上下文){ 返回ListView.builder( itemCount:items.length, item

下面是一个最小的应用程序,表明了我的担忧。如果您运行它,您将看到每个可见项的构建方法都会运行,即使只有一个项的数据发生了更改

导入“包:flift/foundation.dart”;
进口“包装:颤振/材料.省道”;
void main(){
runApp(MyApp());
}
类MyList扩展了无状态小部件{
最后清单项目;
MyList(此.items);
@凌驾
小部件构建(构建上下文){
返回ListView.builder(
itemCount:items.length,
itemBuilder:(上下文,索引){
打印(“构建的项目$index”);
返回列表块(
标题:文本(“${items[index]}”),
);
});
}
}
类MyApp扩展了StatefulWidget{
MyApp({Key?Key}):超级(Key:Key);
@凌驾
_MyAppState createState()=>\u MyAppState();
}
类MyAppState扩展了状态{
列表项=列表.生成(10000,(i)=>i);
void _incrementItem2(){
设置状态(){
这是第[1]++项;
});
}
@凌驾
小部件构建(构建上下文){
最终标题='长列表';
返回材料PP(
标题:标题,,
家:脚手架(
appBar:appBar(
标题:文本(标题),
),
正文:专栏(
儿童:[
MyButton(_incrementItem2),
已展开(子项:MyList(this.items))
],
),
),
);
}
}
类MyButton扩展了无状态小部件{
最终函数递增项2;
空把手{
递增项2();
}
MyButton(this.incrementItem2);
@凌驾
小部件构建(构建上下文){
返回文本按钮(
样式:钮扣样式(
foregroundColor:MaterialStateProperty.all(颜色.蓝色),
),
按下按钮:手把手,
子项:文本(“增量项2”),
);
}
}
在React/React Native中,我将使用shouldComponentUpdate或usemo来防止不必要的渲染。我想在弗利特也这么做

这里有一个链接,指向Flatter github上关于同一主题的已关闭问题: (我将在这里发表评论,并链接到这个问题)

在颤振中避免无用的“重新加载”或调用构建方法的最佳实践是什么,或者考虑到颤振和反应之间的结构差异,这些额外构建是否不是一个严重的问题?

这或多或少类似于几年前关于堆栈溢出的问题:

给出的一个向上投票的答案是使用ListView.builder。虽然这可以防止一些(即使不是最无用的)渲染,但它不能阻止所有无用的渲染,如我的示例所示


提前感谢您的知识和洞察力。

我认为,当您看到性能问题/问题时,请进行优化,但这是有问题的。在这样做之前这样做是不值得的。弗利特团队做了一个测试

请记住,虽然您可能会看到在小部件上发生的重建,您可以使用
print
语句来检测,但您不会看到单独发生的颤振优化渲染

Flatter有一个小部件树、一个元素树和一个renderObject树

小部件树是我们想要构建的手工编码的Dart蓝图。(只有左边树上的蓝色项目。白色项目是基础:我们不直接写。)

元素树是Flatter对我们的蓝图的解释,更详细地介绍了实际使用的组件以及布局和渲染所需的内容

renderObject树将生成一个项目列表,其中包含需要在屏幕上光栅化的项目的尺寸、层深度等详细信息

蓝图可能会被多次擦除和重写,但这并不意味着所有(甚至许多)底层组件都会被销毁/重新创建/重新渲染/重新定位。未更改的组件将被重新使用

假设我们正在建造一个包含水槽和浴缸的浴室。主人决定交换水槽和浴缸的位置。承包商不会使用大锤将陶瓷粉碎,购买/运输并安装全新的水槽和浴缸。他只是拆开每一个,交换它们的位置

下面是一个使用
AnimatedSwitcher
的人为示例:

        child: AnimatedSwitcher(
          duration: Duration(milliseconds: 500),
            child: Container(
                key: key,
                color: color,
                padding: EdgeInsets.all(30),
                child: Text('Billy'))),
AnimatedSwitcher
应该在其子对象更改时交叉淡入淡出。(在本例中从蓝色到绿色)但如果未提供
,则不会重建
动画切换程序
,也不会进行交叉淡入淡出动画

当颤振在元素树上行走并到达
动画切换器
时,它会检查其子元素是否发生了明显变化,即不同的树结构、不同的元素类型等,但它不会进行深入检查(我认为成本太高)。它在相同的树位置(直接子对象)看到相同的对象类型(
容器
),并保留
动画切换器
,而不重建它。结果是
容器
(由于其颜色已更改而重新生成时)从绿色翻转到蓝色,而没有动画

在分析
动画切换器
子树进行优化时,向
容器
添加唯一的
,有助于颤振识别它们是否不同。现在,“颤振”将在500毫秒的持续时间内重建
AnimatedSwitcher
大约30次,以直观地显示动画。(颤振的目标是在大多数设备上达到60 fps。)

下面是复制/粘贴示例中使用的上述
动画切换器

  • 第一次加载时,长方体从绿色翻转到蓝色,没有动画(关键点未更改)
  • 按AppBar中的刷新按钮重置页面,但蓝色
    容器的按键将被更新,从而允许Flatter发现差异并为
    动画切换器进行重建
导入“包装:颤振/材料.省道”;
类AnimatedSwitcherPage扩展StatefulWidget{
@凌驾
_AnimatedSwitcherPageState createState()=>_AnimatedSwitcherPageState();
}
import 'package:flutter/material.dart';

class AnimatedSwitcherPage extends StatefulWidget {
  @override
  _AnimatedSwitcherPageState createState() => _AnimatedSwitcherPageState();
}

class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
  Color color = Colors.lightGreenAccent;
  ValueKey key;

  @override
  void initState() {
    super.initState();
    key = ValueKey(color.value);

    Future.delayed(Duration(milliseconds: 1500), () => _update(useKey: false));
    // this initial color change happens WITHOUT key changing
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animation Switcher'),
        actions: [
          IconButton(icon: Icon(Icons.refresh), onPressed: _restartWithKey)
        ],
      ),
      body: Center(
        child: AnimatedSwitcher(
          duration: Duration(milliseconds: 500),
            child: Container(
                key: key,
                color: color,
                padding: EdgeInsets.all(30),
                child: Text('Billy'))),
      ),
    );
  }

  void _update({bool useKey}) {
    setState(() {
      color = Colors.lightBlueAccent;
      if (useKey) key = ValueKey(color.value);
    });
  }

  void _restartWithKey() {
    setState(() {
      color = Colors.lightGreenAccent; // reset color to green
    });
    Future.delayed(Duration(milliseconds: 1500), () => _update(useKey: true));
    // this refresh button color change happens WITH a key change
  }
}
class MyListTile extends StatelessWidget {
  final int index;
  MyListTile(this.index):super(key:ValueKey(index));

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ItemBloc, ItemState>(
      buildWhen: (previous, current) {
        return previous.items[index] != current.items[index];
      },
      builder: (context, state) {
        return ListTile(title: Text('${state.items[index]}'));
      },
    );
  }
}