flutter - 如何防止 Flutter 中不必要的渲染?
问题描述
下面是一个展示我关注的最小应用程序。如果你运行它,你会看到每个可见项目的构建方法都会运行,即使只有一个项目的数据发生了变化。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyList extends StatelessWidget {
final List<int> items;
MyList(this.items);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
print("item $index built");
return ListTile(
title: Text('${items[index]}'),
);
});
}
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<int> items = List<int>.generate(10000, (i) => i);
void _incrementItem2() {
setState(() {
this.items[1]++;
});
}
@override
Widget build(BuildContext context) {
final title = 'Long List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: <Widget>[
MyButton(_incrementItem2),
Expanded(child: MyList(this.items))
],
),
),
);
}
}
class MyButton extends StatelessWidget {
final Function incrementItem2;
void handlePress() {
incrementItem2();
}
MyButton(this.incrementItem2);
@override
Widget build(BuildContext context) {
return TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
),
onPressed: handlePress,
child: Text('increment item 2'),
);
}
}
在 React / React Native 中,我会使用 shouldComponentUpdate 或 UseMemo 来防止不必要的渲染。我想在 Flutter 中做同样的事情。
这是 Flutter github 上关于同一主题的已关闭问题的链接:(我将在此处评论该问题的链接) https://github.com/flutter/flutter/issues/19421
在 Flutter 中避免无用的“重新渲染”或调用 build 方法的最佳实践是什么,或者考虑到 Flutter 和 React 之间的结构差异,这些额外的构建是否不是一个严重的问题?
这或多或少类似于几年前在堆栈溢出中提出的这个问题:How to prevent rerender of a widget based on custom logic in flutter?
一个赞成的答案是使用 ListView.builder。虽然这可以阻止一些即使不是大多数无用的渲染,但它并不能阻止所有无用的渲染,如我的示例所示。
提前感谢您的知识和洞察力。
解决方案
我会争辩说,当您看到性能问题/ jankiness 时进行优化,但这是有可能的。在这样做之前不值得这样做。Flutter 团队为此做了一个简短的视频。
请记住,虽然您可能会看到您可以使用print
语句检测到的小部件上发生重建,但您并没有看到 Flutter 优化渲染是单独发生的。
Flutter 有一个小部件树、一个元素树和一个渲染对象树。
小部件树是我们想要构建的手动编码的 Dart 蓝图。(左侧树上只有蓝色项目。白色项目是底层:我们不直接写。)
元素树是 Flutter 对我们蓝图的解释,更详细地说明了实际使用的组件以及布局和渲染所需的内容。
renderObject 树将生成一个项目列表,其中包含其尺寸、层深度等需要光栅化到屏幕上的详细信息。
蓝图可能会被多次擦除和重写,但这并不意味着所有(甚至许多)底层组件都被销毁/重新创建/重新渲染/重新定位。未更改的组件将被重新使用。
假设我们正在建造一个包含水槽和浴缸的浴室。业主决定交换水槽和浴缸的位置。承包商不用大锤粉碎陶瓷,购买/运输并安装全新的水槽和浴缸。他只是分离每个并交换它们的位置。
这是一个人为的例子,使用AnimatedSwitcher
:
child: AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: Container(
key: key,
color: color,
padding: EdgeInsets.all(30),
child: Text('Billy'))),
AnimatedSwitcher
当他们改变时,应该交叉淡化它的孩子。(在这种情况下说从蓝色到绿色。)但如果key
没有提供,AnimatedSwitcher
则不会重建,也不会做交叉淡入淡出动画。
当 Flutter 遍历元素树并到达 时AnimatedSwitcher
,它会检查它的子元素是否发生了明显的变化,即不同的树结构、不同的元素类型等,但它不会进行深度检查(我认为成本太高了)。它在相同的树位置(直接子级)中看到相同的对象 Type ( Container
) 并保留AnimatedSwitcher
而不重建它。结果是Container
(当它重新构建时,因为它的颜色已经改变)从绿色翻转到蓝色,没有动画。
添加唯一的key
toContainer
有助于 Flutter 在分析AnimatedSwitcher
子树以进行优化时将它们识别为不同的。现在 Flutter 将AnimatedSwitcher
在其 500 毫秒的持续时间内重建大约 30 次以直观地显示动画。(Flutter 的目标是在大多数设备上达到 60fps。)
AnimatedSwitcher
这是复制/粘贴示例中使用的上述内容:
- 在第一次加载时,盒子从绿色翻转到蓝色,没有动画(键没有改变)
- 按下 AppBar 中的刷新按钮以重置页面,但键已更新为 blue
Container
,从而允许 Flutter 发现差异并为AnimatedSwitcher
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
}
}
意见附录
问题:
我知道除非键不同,否则它不会做动画,但如果它没有重建,那么颜色为什么/如何从绿色切换到蓝色?
回复:
AnimatedSwitcher
是一个StatefulWidget
。它在构建之间保存前一个子小部件。当一个新的构建发生时,它会比较旧的孩子和新的孩子。(参见Widget.canUpdate()
比较方法。)
如果新旧孩子不同/不同,它会执行一个过渡动画,用于 X 帧/滴答声。
如果旧孩子和新孩子不是不同的/不同的(根据.canUpdate()
),AnimatedSwitcher
基本上是一个不需要的包装器:它不会做任何特别的事情,但会进行重建以维护其最新的孩子(?)。我不得不承认这是一个糟糕的例子选择。
推荐阅读
- android - 在 Android 中删除 Firebase 令牌
- node.js - 用户登录后处理 MEAN 堆栈应用程序中的社交媒体集成
- python - 使用 python 从同一网络上可访问的 c$ 计算机读取文本文件
- c# - 我的身份验证表单未加载 CSS
- java - 如何解决 Java 中的编码问题?
- r - 使用按列分组识别行中的差异
- reactjs - React: Unable to access and test props & state during Jest/Enzyme shallow rendering
- android - set center_horizontal for child of FrameLayout
- google-maps - 用于识别道路阻塞/封闭标志的 Google API
- rabbitmq - rabbitmq 的健康检查