MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter减少Widget重建的技巧:提升UI渲染效率

2022-05-113.7k 阅读

理解 Flutter 中的 Widget 重建

在 Flutter 开发中,Widget 是构建用户界面的基本元素。Flutter 的响应式编程模型意味着当状态发生变化时,Widget 树会进行重建。虽然这种机制提供了强大的灵活性和高效的更新方式,但过度的 Widget 重建会导致性能问题,尤其是在复杂 UI 场景下。

Widget 重建的本质源于 Flutter 的设计理念,它基于不可变数据结构。当状态改变时,Flutter 通过创建新的 Widget 树来描述 UI 的新状态。例如,考虑一个简单的计数器应用:

import 'package:flutter/material.dart';

class CounterApp extends StatefulWidget {
  @override
  _CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _count = 0;

  void _incrementCounter() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_count',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

每次点击 FloatingActionButton_count 状态改变,setState 方法被调用,整个 CounterAppbuild 方法会重新执行,意味着整个 Scaffold 及其子 Widget 都会重建。

不必要 Widget 重建带来的问题

  1. 性能损耗:Widget 重建涉及创建新的对象、计算布局和绘制,这对 CPU 和 GPU 都是额外的负担。在复杂 UI 中,如包含大量列表项或动画的界面,频繁重建会导致卡顿,降低用户体验。
  2. 资源浪费:每次重建都需要分配新的内存来存储新的 Widget 对象,即使部分 Widget 并没有实际变化。这会增加内存使用,长期运行可能导致内存泄漏或应用崩溃。

使用 const 和 final Widgets

const Widgets

在 Flutter 中,const Widgets 是不可变的,且在编译时就确定其值。当 Widget 树中存在 const Widget 时,Flutter 框架可以更高效地处理它们,因为它们不会因为状态变化而改变。例如:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Using const'),
        ),
        body: Center(
          child: Text('This is a const text'),
        ),
      ),
    );
  }
}

在上述代码中,MaterialAppScaffoldAppBarText 都是 const Widget。这意味着无论应用状态如何变化,这些 Widget 都不会重建。即使父 Widget 重建,只要这些 const Widget 的引用没有改变,它们就不会重新创建。

final Widgets

final Widgets 与 const Widgets 类似,它们也是不可变的。但 final Widgets 的值在运行时确定,而不是编译时。例如:

class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage({required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text('This is a final widget'),
      ),
    );
  }
}

这里的 titlefinal,只要 MyHomePagetitle 属性不变,AppBar 中的 Text Widget 就不会因为父 Widget 的重建而重建。虽然 final Widgets 不如 const Widgets 性能优化程度高,但在很多场景下也能显著减少重建。

利用 Key 控制 Widget 重建

了解 Key 的作用

在 Flutter 中,Key 是一个标识 Widget 的对象。当 Widget 树发生变化时,Flutter 框架使用 Key 来判断哪些 Widget 可以复用,哪些需要重建。有两种主要类型的 KeyLocalKeyGlobalKey

LocalKey 用于在同一父 Widget 下标识 Widget。例如,ValueKeyObjectKey 属于 LocalKeyGlobalKey 则在整个应用的 Widget 树中唯一标识一个 Widget。

使用 ValueKey 减少重建

考虑一个包含多个文本 Widget 的列表:

class MyList extends StatelessWidget {
  final List<String> items;
  MyList({required this.items});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(items[index]),
        );
      },
    );
  }
}

如果列表中的项顺序或内容发生变化,Flutter 会重建整个列表项。但通过使用 ValueKey,我们可以优化这个过程:

class MyList extends StatelessWidget {
  final List<String> items;
  MyList({required this.items});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return ListTile(
          key: ValueKey(items[index]),
          title: Text(items[index]),
        );
      },
    );
  }
}

现在,当列表项的内容发生变化时,只有对应的 ListTile 会重建,而不是整个列表。因为 ValueKey 使得 Flutter 框架能够识别出哪些 Widget 发生了变化,哪些可以复用。

GlobalKey 的应用场景

GlobalKey 适用于需要在 Widget 树的不同部分访问同一个 Widget 的场景。例如,在一个复杂的表单应用中,可能有一个提交按钮在页面底部,而表单的验证逻辑在表单的各个输入项 Widget 中。通过为表单输入项 Widget 使用 GlobalKey,提交按钮可以直接访问这些输入项的状态,而无需通过复杂的状态管理机制来传递数据。

class MyFormWidget extends StatefulWidget {
  const MyFormWidget({Key? key}) : super(key: key);

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

class _MyFormWidgetState extends State<MyFormWidget> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter some text';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Processing Data')),
                );
              }
            },
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}

在这个例子中,GlobalKey 用于 Form Widget,使得提交按钮可以直接调用 FormStatevalidate 方法,而无需在不同层次的 Widget 间传递状态。

状态管理与 Widget 重建优化

选择合适的状态管理方案

Flutter 提供了多种状态管理方案,如 setStateInheritedWidgetProviderBloc 等。不同的状态管理方案对 Widget 重建的影响不同。

  1. setState:简单直观,但会导致整个 StatefulWidget 重建。在复杂 UI 中,这可能会引发性能问题。例如在前面的计数器应用中,每次调用 setState 都会重建整个 CounterAppbuild 方法。
  2. InheritedWidget:适用于在 Widget 树中向下传递数据,避免不必要的重建。它通过缓存数据,只有当数据真正改变时,依赖该数据的 Widget 才会重建。例如,在一个多屏幕应用中,如果需要在不同屏幕间共享用户主题设置,可以使用 InheritedWidget 来传递主题数据,只有需要显示主题相关 UI 的 Widget 会在主题改变时重建。
  3. Provider:基于 InheritedWidget 封装,提供更便捷的状态管理方式。它允许在 Widget 树的任意位置获取和更新状态,同时控制哪些 Widget 因状态变化而重建。例如,在一个电商应用中,购物车状态可以通过 Provider 管理,只有购物车页面和相关的购物车图标 Widget 会在购物车状态改变时重建。
  4. Bloc:采用业务逻辑与 UI 分离的方式,通过事件驱动状态变化。它能有效减少因业务逻辑变化导致的 UI 重建。例如,在一个社交应用中,用户登录逻辑在 Bloc 中处理,只有当登录状态真正影响到 UI 显示(如显示登录成功后的用户信息)时,相关 UI 才会重建。

使用 Provider 优化 Widget 重建

以下是一个使用 Provider 管理计数器状态的示例:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Counter with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('Counter App with Provider'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Consumer<Counter>(
                builder: (context, counter, child) {
                  return Text(
                    '${counter.count}',
                    style: Theme.of(context).textTheme.headline4,
                  );
                },
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<Counter>().increment(),
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

在这个例子中,Counter 类继承自 ChangeNotifier 来管理状态。ChangeNotifierProviderCounter 实例提供给整个 Widget 树。Consumer Widget 只在 Counter 状态变化时重建,而不是像 setState 那样整个 Scaffold 重建。这样,在复杂 UI 中,只有依赖计数器状态的部分会重建,大大提高了渲染效率。

局部重建与 CustomSingleChildLayout

局部重建的原理

在某些情况下,我们可能只需要重建 Widget 树的一部分,而不是整个树。Flutter 提供了一些机制来实现局部重建,其中 CustomSingleChildLayout 是一种强大的工具。

CustomSingleChildLayout 允许我们自定义单个子 Widget 的布局,并且可以通过 RelayoutBoundary 来控制子 Widget 的重建范围。当父 Widget 重建时,RelayoutBoundary 会阻止其内部的子 Widget 不必要的重建,除非子 Widget 的依赖发生变化。

使用 CustomSingleChildLayout 实现局部重建

考虑一个包含图片和描述文本的卡片 Widget,假设图片是静态的,而描述文本可能会因为某些事件而改变。我们可以使用 CustomSingleChildLayout 来实现只重建描述文本部分:

import 'package:flutter/material.dart';

class CardLayoutDelegate extends SingleChildLayoutDelegate {
  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return BoxConstraints(
      maxWidth: constraints.maxWidth,
      maxHeight: constraints.maxHeight,
    );
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset.zero;
  }

  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
    return false;
  }
}

class MyCard extends StatefulWidget {
  @override
  _MyCardState createState() => _MyCardState();
}

class _MyCardState extends State<MyCard> {
  String _description = 'Initial description';

  void _updateDescription() {
    setState(() {
      _description = 'Updated description';
    });
  }

  @override
  Widget build(BuildContext context) {
    return CustomSingleChildLayout(
      delegate: CardLayoutDelegate(),
      child: Column(
        children: [
          Image.asset('assets/image.jpg'),
          RelayoutBoundary(
            child: Text(_description),
          ),
          ElevatedButton(
            onPressed: _updateDescription,
            child: Text('Update Description'),
          ),
        ],
      ),
    );
  }
}

在这个例子中,CustomSingleChildLayout 负责整体布局,RelayoutBoundary 包裹描述文本 Text Widget。当点击按钮更新描述文本时,只有 RelayoutBoundary 内部的 Text Widget 会重建,而图片不会重建,从而提升了渲染效率。

避免不必要的 build 方法调用

理解 build 方法的调用时机

在 Flutter 中,build 方法是 StatelessWidgetStatefulWidget 构建 UI 的核心方法。StatelessWidgetbuild 方法在每次 Widget 树需要重建时都会被调用。对于 StatefulWidget,当调用 setState 或者父 Widget 重建导致 State 对象的 didUpdateWidget 方法被调用时,build 方法会被执行。

优化 build 方法中的计算

  1. 缓存计算结果:如果 build 方法中有一些复杂的计算,如数据排序、格式化等,这些计算结果可以被缓存起来,避免每次重建时重复计算。例如:
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  List<int> _data = [3, 1, 4, 1, 5, 9];
  List<int> _sortedData = [];

  @override
  void initState() {
    super.initState();
    _sortedData = List.from(_data)..sort();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Sorted data: $_sortedData'),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _data.add(2);
              _sortedData = List.from(_data)..sort();
            });
          },
          child: Text('Add data'),
        ),
      ],
    );
  }
}

在这个例子中,_sortedDatainitState 中计算并缓存,只有当数据发生变化时才重新计算,避免了每次 build 都进行排序操作。

  1. 条件构建:根据条件选择性地构建 Widget。例如,在一个用户设置页面中,某些高级设置选项可能只对高级用户可见。可以通过条件判断来避免不必要的 Widget 构建:
class UserSettings extends StatelessWidget {
  final bool isAdvancedUser;

  UserSettings({required this.isAdvancedUser});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('General settings'),
        if (isAdvancedUser)
          Column(
            children: [
              Text('Advanced setting 1'),
              Text('Advanced setting 2'),
            ],
          ),
      ],
    );
  }
}

这样,当 isAdvancedUserfalse 时,高级设置部分的 Widget 不会被构建,减少了不必要的计算和重建。

总结与最佳实践

通过以上各种技巧,我们可以有效地减少 Flutter 应用中 Widget 的重建,提升 UI 渲染效率。以下是一些最佳实践总结:

  1. 优先使用 constfinal Widgets:在可能的情况下,将 Widget 定义为 constfinal,避免不必要的重建。
  2. 合理使用 Key:根据场景选择合适的 Key,如 ValueKey 用于列表项优化,GlobalKey 用于跨 Widget 树访问。
  3. 选择合适的状态管理方案:根据应用的复杂度和需求,选择 setStateInheritedWidgetProviderBloc 等状态管理方案,控制状态变化对 UI 重建的影响。
  4. 利用局部重建机制:使用 CustomSingleChildLayoutRelayoutBoundary 实现局部重建,减少整体重建开销。
  5. 优化 build 方法:缓存计算结果,进行条件构建,避免在 build 方法中进行不必要的计算。

通过遵循这些最佳实践,Flutter 开发者可以打造出高性能、流畅的用户界面,提升应用的整体质量和用户体验。