将Flutter推向极限:每个开发人员都应该知道的性能提示

有没有觉得 Flutter 是比赛中的乌龟?别担心!只要我们有几招,我们就能把乌龟变成涡轮增压的兔子。准备好放大了吗?让我们深入了解一些 Flutter 性能技巧!

Flutter 应用程序在默认情况下是高性能的,因此您只需要避免常见的陷阱即可获得出色的性能。如何设计和实现应用的 UI 会对应用的运行效率产生很大影响。

这些最佳实践建议将帮助您编写最高性能的 Flutter 应用程序。

让我们开始阅读吧!!

1.使用 Clean Architecture(干净架构)

Clean Architecture 是一种软件设计模式,强调关注点分离和独立测试。此模式鼓励将应用程序逻辑分离到不同的层中,每一层负责一组特定的任务。Clean Architecture 非常适合大型应用程序,因为它提供了清晰的关注点分离,并使测试更容易。

您可以查看此软件包 - clean_architecture_scaffold

以下是 Flutter 中的一个 Clean Architecture 实现示例:

lib/
  data/
    models/
      user_model.dart
    repositories/
      user_repository.dart
  domain/
    entities/
      user.dart
    repositories/
      user_repository_interface.dart
    usecases/
      get_users.dart
  presentation/
    pages/
      users_page.dart
    widgets/
      user_item.dart
    main.dart

2.使用良好的状态管理

状态管理在 Flutter 应用性能中起着至关重要的作用。根据应用的复杂性选择正确的状态管理方法。对于中小型应用程序,内置的 setState 方法可能就足够了。但是,对于更大更复杂的应用程序,可以考虑使用状态管理库,如 bloc 或 riverpod。

// Bad Approach
setState(() {
  // Updating a large data structure unnecessarily
  myList.add(newItem);
});

// Better Approach
final myListBloc = BlocProvider.of(context);
myListBloc.add(newItem);

3.使用代码分析工具提高代码质量

代码分析工具,如 Flutter Analyzer 和 Lint,对于提高代码质量和降低 bug 和错误的风险非常有帮助。这些工具可以帮助在潜在问题成为问题之前识别它们,还可以提供改进代码结构和可读性的建议。

以下是在 Flutter 中使用 Flutter Analyzer 的示例:

flutter analyze lib/

4.使用自动化测试提高代码可靠性

自动化测试是构建大型应用程序的重要组成部分,因为它有助于确保代码可靠并按预期执行。Flutter 为自动化测试提供了几个选项,包括单元测试、小部件测试和集成测试。

下面是使用 Flutter Test 包进行自动化测试的示例:

void main() {
  test('UserRepository returns a list of users', () {
    final userRepository = UserRepository();
    final result = userRepository.getUsers();
    expect(result, isInstanceOf>());
  });
}

5.使用 Flutter Inspector 进行调试

Flutter Inspector 是一个强大的工具,用于调试 Flutter 应用程序。它允许开发人员检查和操作小部件树,查看性能指标等。Flutter Inspector 可以通过 Flutter DevTools 浏览器扩展或通过命令行访问。

下面是使用 Flutter Inspector 进行调试的示例:

flutter run --debug

6.延迟加载和分页

一次获取和呈现大量数据会显著影响性能。实现延迟加载和分页以根据需要加载数据,特别是对于长列表或数据密集型视图。

// Bad Approach
// Fetch and load all items at once.
List allItems = fetchAllItems();

// Better Approach
// Implement lazy loading and pagination.
List loadItems(int pageNumber) {
  // Fetch and return data for the specific page number.
}

// Use a ListView builder with lazy loading.
ListView.builder(
  itemCount: totalPages,
  itemBuilder: (context, index) {
    return FutureBuilder(
      future: loadItems(index),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          // Build your list item here.
        } else {
          return CircularProgressIndicator();
        }
      },
    );
  },
);

7.缩小图像大小

大型图像文件可能会降低应用的性能,尤其是在加载多个图像时。压缩和调整图像大小,以减少其文件大小,而不会影响太多的质量。

示例:假设您有一个高分辨率的图像,但您只需要将其显示在应用程序中的较小容器中。您可以使用 flutter_image_compress 库来调整其大小,而不是使用原始的高分辨率图像。

import 'package:flutter_image_compress/flutter_image_compress.dart';

// Original image file
var imageFile = File('path/to/original/image.png');
// Get the image data
var imageBytes = await imageFile.readAsBytes();
// Resize and compress the image
var compressedBytes = await FlutterImageCompress.compressWithList(
  imageBytes,
  minHeight: 200,
  minWidth: 200,
  quality: 85,
);
// Save the compressed image to a new file
var compressedImageFile = File('path/to/compressed/image.png');
await compressedImageFile.writeAsBytes(compressedBytes);

8.优化动画

避免使用可能影响应用性能的繁重或复杂的动画,尤其是在较旧的设备上。明智地使用动画,并考虑使用 Flutter 的内置动画,如 AnimatedContainerAnimatedOpacity 等。

// Bad Approach
// Using an expensive animation
AnimatedContainer(
  duration: Duration(seconds: 1),
  height: _isExpanded ? 300 : 1000,
  color: Colors.blue,
);
// Better Approach
// Using a simple and efficient animation
AnimatedContainer(
  duration: Duration(milliseconds: 500),
  height: _isExpanded ? 300 : 100,
  color: Colors.blue,
);

9.优化应用程序启动时间

通过优化初始化过程减少应用的启动时间。使用 flutter_native_splash 包在应用加载时显示闪屏,并将非必要组件的初始化延迟到应用启动后。

10.避免使用深树,而是创建一个单独的小部件

你不想继续滚动你的 IDE 与一千行代码。尝试创建一个单独的小部件。它将看起来干净,易于重构。

//Bad
Column(
  children: [
    Container(
      //some lengthy code here
    ),
    Container(
      //some another lengthy code
    ),
    //some another lengthy code
  ],
)

//Good
Column(
  children: [
    FirstLengthyCodeWidget(),
    SecondLengthyCodeWidget(),
    //another lengthy code widget etc
  ],
)

11.使用级联(..)

如果你刚开始使用 flutter,你可能还没有使用这个操作符,但是当你想在同一个对象上执行一些任务时,它非常有用。

//Bad
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

//Good
var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

12.使用展开运算符(...)

这是 dart 提供的另一个漂亮的操作符。您可以简单地使用此操作符执行许多任务,例如 if-else、加入列表等。

//Bad
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        isTrue ? const Text('One') : Container(),
        isTrue ? const Text('Two') : Container(),
        isTrue ? const Text('Three') : Container(),
      ],
    ),
  );
}

//Good
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        if(isTrue)...[
          const Text('One'),
          const Text('Two'),
          const Text('Three')
        ]
      ],
    ),
  );
}

13.避免使用硬编码的样式、装饰等

如果您在应用程序中使用硬编码的样式、装饰等,并且稍后决定更改这些样式。你会一个接一个地修复它们。

//Bad
Column(
  children: const [
    Text(
      'One',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
    Text(
      'Two',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
  ],
)

//Good
Column(
  children: [
    Text(
      'One',
      style: Theme.of(context).textTheme.subtitle1,
    ),
    Text(
      'Two',
      style: Theme.of(context).textTheme.subtitle1,
    ),
  ],
),

14.小心使用 build()

避免使用过大的单个小部件和较大的 build() 函数。根据封装以及它们的变化方式将它们拆分为不同的小部件。

当在 State 对象上调用 setState() 时,所有派生小部件都会重新生成。因此,将 setState() 调用本地化到 UI 实际需要更改的子树部分。如果更改只包含在树的一小部分中,请避免在树的较高位置调用 setState()

让我们看看这个例子,我们希望当用户按下图标时,只有这个图标的颜色会改变。

因此,如果我们在一个小部件中拥有所有这些 UI,当按下图标时,它将更新整个 UI。我们可以做的是将图标分隔为 StatefulWidget

之前

import 'package:flutter/material.dart';

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

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

class _FidgetWidgetState extends State {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('App Title'),
      ),
      body: Column(
        children: [
          Text('Some Text'),
          IconButton(
            onPressed: () => setState(() {
              // Some state change here
            }),
            icon: Icon(Icons.favorite),
          ),
        ],
      ),
    );
  }
}

之后

import 'package:flutter/material.dart';

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

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

class _MyIconWidgetState extends State {
  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: () => setState(() {

      }),
      icon: Icon(Icons.favorite),
    );
  }
}

15.使用 Widgets 而不是函数

您可以节省 CPU 周期,并与构造函数一起使用,在需要时进行重建,还有更多好处(重复使用等......)。

//Bad
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      _getHeader(),
      _getSubHeader(),
      _getContent()
    ]
   );
}

//Good
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      HeaderWidget(),
      SubHeaderWidget(),
      ContentWidget()
    ]
  );
}
正如 Riverpod、Provider 和其他软件包的创建者 Remi Rousselet 所说。“类有更好的默认行为。使用方法的唯一好处就是可以少写一点代码。没有任何功能上的好处”。

16.尽可能使用 final

使用 final 关键字可以大大提高应用的性能。当一个值被声明为 final 时,它只能被设置一次,此后不会改变。这意味着框架不需要经常检查更改,从而提高了性能。

final items = ["Item 1", "Item 2", "Item 3"];

在这个例子中,变量项被声明为 final ,这意味着它的值不能被改变。这提高了性能,因为框架不需要检查此变量的更改。

17.尽可能使用 const

x = Container();
y = Container();
x == y // false

x = const Container();
y = const Container();
x == y // true

const widget 在编译时创建,因此在运行时速度更快。

18.尽可能使用 const 构造函数

class CustomWidget extends StatelessWidget {
  const CustomWidget();

  @override
  Widget build(BuildContext context) {
    ...
  }
}

当构建自己的小部件或使用 Flutter 小部件时。这有助于 Flutter 只重建应该更新的小部件。

19.尽可能使用私有变量/方法

除非必要,否则尽可能使用 private 关键字。

//Bad
class Student {
  String name;
  String address;
  Student({
      required this.name,
      required this.address,
    });
  }
}

//Good
class Student{
  String _name;
  String _address;
  Student({
    required String name,
    required String address,
  }):
  _name = name,
  _address = address;
}

是的,与性能相比,它更像是 Dart 的最佳实践。但是,最佳实践可以在某种程度上提高性能,比如理解代码、降低复杂性等。

20.使用 nil 代替 const Container()

// good
text != null ? Text(text) : const Container()
// Better
text != null ? Text(text) : const SizedBox()
// BEST
text != null ? Text(text) : nil
or
if (text != null) Text(text)

它只是一个基本的元素小部件,几乎没有成本。查看包 - nil

21.在 ListView 中对长列表使用 itemExtent

这有助于 Flutter 计算 ListView 滚动位置,而不是计算每个小部件的高度,并使滚动动画更具性能。

默认情况下,每个孩子都必须确定其范围,这在性能方面是相当昂贵的。显式设置该值可以节省大量 CPU 周期。列表越长,使用此属性获得的速度就越快。

//Nope
final List _listItems = [1, 2, 3, 4, 5, 6, 7, 8, 9];

@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: _listItems.length,
    itemBuilder: (context, index) {
      var item = _listItems[index];
      return Center(
        child: Text(item.toString())
      );
    }
}

//Good
final List _listItems = [1, 2, 3, 4, 5, 6, 7, 8, 9];

@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemExtent: 150,
    itemCount: _listItems.length,
    itemBuilder: (context, index) {
      var item = _listItems[index];
      return Center(
        child: Text(item.toString())
      );
    }
}

22.避免将 AnimationController 与 setState 一起使用

这不仅会导致重新构建整个 UI 的动画部件,而且会使动画滞后。

void initState() {
  _controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 1),
  )..addListener(() => setState(() {}));
}

Column(
  children: [
    Placeholder(), // rebuilds
    Placeholder(), // rebuilds
    Placeholder(), // rebuilds
    Transform.translate( // rebuilds
      offset: Offset(100 * _controller.value, 0),
      child: Placeholder(),
    ),
  ],
),

void initState() {
  _controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 1),
  );
  // No addListener(...)
}

AnimatedBuilder(
  animation: _controller,
  builder: (_, child) {
  return Transform.translate(
    offset: Offset(100 * _controller.value, 0),
    child: child,
    );
  },
  child: Placeholder(),
),

23.使用 Keys 加速 Flutter 性能

使用 Keys 时,Flutter 能更好地识别小部件。这让我们的性能提高了 4 倍。

// FROM
return value ? const SizedBox() : const Placeholder(),
// TO
return value ? const SizedBox(key: ValueKey('SizedBox')) : const Placeholder(key: ValueKey('Placeholder')),
----------------------------------------------
// FROM
final inner = SizedBox();
return value ? SizedBox(child: inner) : inner,
// TO
final global = GlobalKey();
final inner = SizedBox(key: global);
return value ? SizedBox(child: inner) : inner,
谨慎:
ValueKey 会让你的代码看起来有点臃肿
GlobalKey 有点危险,但有时候值得。

24.使用图像 ListView 时优化内存

ListView.builder(
  ...
  addAutomaticKeepAlives: false (true by default)
  addRepaintBoundaries: false (true by default)
);

ListView 无法杀死屏幕上不可见的子代。如果子代具有高分辨率图像,就会消耗大量内存。

通过将这些选项设置为 false,可能会导致使用更多的 GPU 和 CPU 工作,但它可以解决我们的内存问题,并且您将获得非常高性能的视图,而不会出现明显的问题。

25.使用 for/while 代替 foreach/map

如果你要处理大量的数据,使用正确的循环可能会影响你的性能。

26.预缓存您的图像和图标

这取决于具体情况,但我一般会在主系统中预先缓存所有图像。

对于图片

你不需要任何包装,只要用-

precacheImage(
  AssetImage(imagePath),
  context
);

对于 SVG

您需要 flutter_svg

precachePicture(
  ExactAssetPicture(SvgPicture.svgStringDecoderBuilder, iconPath),
  context
);

27.使用 SKSL 预热

flutter run --profile --cache-sksl --purge-persistent-cache
flutter build apk --cache-sksl --purge-persistent-cache

如果一个应用程序在第一次运行时有不稳定的动画,然后在相同的动画中变得平滑,那么它很可能是由于着色器编译的不稳定。

28.考虑使用 RepaintBoundary

此小部件为其子小部件创建单独的显示列表,这可以提高特定情况下的性能。

29.如果可能,请使用生成器命名的构造函数

Listview.builder()

生成器只会在屏幕上渲染显示的项目。即使看不到,也能显示所有的孩子。

30.不要使用 ShrinkWrap 任何可滚动的小部件

测量内容问题。

31.使用重载函数时使用 ISOLATE (隔离)

有些方法非常昂贵,比如图像处理,它们可能会让应用程序在主线程中冻结。如果不想出现这种情况,就应该考虑使用隔离。

32.不要为每一件小事使用隔离

但如果您在任何地方都使用隔离区,即使是最小的操作,您的应用程序也会变得非常笨拙。因为生成隔离区的操作并不便宜。这需要时间和资源。

33.正确处理数据

不必要的内存使用会悄无声息地杀死应用程序内部的数据。因此,不要忘记处理数据。

34.压缩数据以节省内存

final response = await rootBundle.loadString('assets/en_us.json');

final original = utf8.encode(response);

final compressed = gzip.encode(original);
final decompress = gzip.decode(compressed);

final enUS = utf8.decode(decompress);

这样还可以节省一些内存。

35.保持更新 Flutter

每一个版本的 Flutter 都会变得越来越快。因此,不要忘了及时更新您的 Flutter 版本,让您的作品更加精彩。

36.在真实的设备上测试性能

始终在真实的设备(包括较旧型号)上测试应用的性能,以确定在模拟器或较新设备上可能不明显的任何性能问题。

37.首选 StatelessWidget 优于 StatefulWidget

StatelessWidgetStatefulWidget 更快,因为顾名思义,它不需要管理状态。这就是为什么如果可能的话,你应该选择它的原因。

选择 StatefulWidget 时...

你需要一个准备功能 initState()

您需要使用 dispose() 处理资源

您需要使用 setState() 触发小部件重建

你的小部件有变化的变量(非 final)

在所有其他情况下,您应该首选 StatelessWidget

38.不使用 OpacityWidget

当与动画一起使用时, Opacity 小部件可能会导致性能问题,因为 Opacity 小部件的所有子小部件也将在每个新帧中重建。在这种情况下,最好使用 AnimatedOpacity 。如果要淡入图像,请使用 FadeInImage 小部件。如果您希望有一个不透明度的颜色,请绘制一个不透明度的颜色。

//Bad
Opacity(opacity: 0.5, child: Container(color: Colors.red))
//Good
Container(color: Color.fromRGBO(255, 0, 0, 0.5))

39.首选 SizedBox 而不是 Container

Container widget 非常灵活。例如,您可以自定义填充或边框,而无需将其嵌套在另一个小部件中。但如果你只需要一个具有一定高度和宽度的盒子,最好使用 SizedBox 小部件。它可以成为常量,而 Container 不能。

要在 Row/Column, 中添加空白,请使用 SizedBox 而不是 Container

//NOPE
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      Text(header),
      Container(height: 10),
      Text(subheader),
      Text(content)
    ]
  );
}

//YES~
@override
Widget build(BuildContext context) {
return Column(
  children: [
      Text(header),
      const SizedBox(height: 10),
      Text(subheader),
      Text(content)
    ]
  );
}

40.不要使用剪裁

剪裁是一个非常昂贵的操作,当你的应用变慢时应该避免。如果将剪切行为设置为 Clip.antiAliasWithSaveLayer,则代价会更高。试着寻找其他的方法来实现你的目标。例如,一个圆角边框的矩形可以用 borderRadius 属性来代替剪裁。

41.使用 Offstage 小部件

Offstage 小部件允许您隐藏一个小部件,而无需将其从小部件树中移除。这对于提高性能非常有用,因为框架不需要重建隐藏的小部件。

Offstage(
  offstage: !showWidget,
  child: MyWidget(),
)

在本例中,当 showWidget 变量为假时, Offstage 小部件用于隐藏 MyWidget 小部件。这通过减少需要重建的小部件的数量来提高性能。

你有没有想过 OffstageOpacityVisibility widget 之间的区别?在这里你可以找到简短的解释。

在 Flutter 中, Offstage widget 用于在布局中隐藏子 widget,同时它仍然是树的一部分。它可用于有条件地显示或隐藏子部件,而无需重新构建整个树。

Opacity widget 用于控制子 widget 的透明度。它采用一个介于 0.0 和 1.0 之间的值,其中 0.0 表示完全透明,1.0 表示完全不透明。但是,重要的是要注意它可能会影响性能,因此仅在必要时使用它。

Visibility 小部件用于控制子小部件的可见性。它可以用于有条件地显示或隐藏子部件,而不必重新构建整个树。

这三个部件都用于控制子部件的显示,但它们的控制方式不同。Offstage 控制布局,Opacity 控制透明度,Visibility 控制可见性

42.使用 WidgetsBinding.instance.addPostFrameCallback

在某些情况下,我们需要在渲染帧后执行某些操作。既不要尝试使用任何延迟函数,也不要创建自定义回调!我们可以使用 WidgetsBinding.instance.addPostFrameCallback 方法来做到这一点。该回调将在框架渲染后调用,并通过避免不必要的重建来提高性能。

WidgetsBinding.instance.addPostFrameCallback((_) {
 //Perform the action here
});

43.使用 AutomaticKeepAliveClientMixin

当使用 ListViewGridView 时,可以多次构建子对象。为了避免这种情况,我们可以对子部件使用 AutomaticKeepAliveClientMixin 。这将使子部件的状态保持活跃并提高性能。

class MyChildWidget extends StatefulWidget {
  @override
  _MyChildWidgetState createState() => _MyChildWidgetState();
}

class _MyChildWidgetState extends State with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    return Text("I am a child widget");
  }
}

在本例中, MyChildWidget 类使用 AutomaticKeepAliveClientMixin mixin, wantKeepAlive 属性设置为 true 。这将使 MyChildWidget 的状态保持活动状态,并防止它被多次重建,从而提高性能。

44.使用 MediaQuery.sizeOf(context)

当您使用 MediaQuery.of(context).size 时,flutter 会将您的小部件与 MediaQuery 的大小相关联,这在代码库中多次使用时可能会导致不必要的重建。通过使用 MediaQuery.sizeOf(context) ,您可以绕过这些不必要的重建,并增强应用的响应能力。也适用于其他 MediaQuery 方法,例如使用 .platformBrightnessOf(context) 而不是 .of(context).

好吧,但是如何测量它们呢?

要衡量 Dart/Flutter 应用程序的性能,可以使用性能视图。它提供了显示 Flutter 帧和时间线事件的图表。

不要在调试模式下测量性能

有一种用于性能和内存测量的特殊模式,即 Profile 模式。您可以通过 Android Studio 或 Visual Studio Code 等 IDE 或执行以下 CLI 命令来运行它:

flutter run - profile

在调试模式下,应用程序未被优化,因此通常比其他模式运行得慢。

不要在模拟器中测量性能

运行编译后的应用程序检查性能问题时,请勿使用模拟器。模拟器无法像真实设备一样再现真实世界的行为。在实际设备上运行时,您可能会发现一些并非真正的问题。模拟器也不支持配置文件模式。

结论部分

优化 Flutter 应用的性能对于提供无缝用户体验至关重要。通过实施这些提示,您可以进一步优化 Flutter 应用的性能。请记住,性能优化是一个持续的过程,定期的性能分析和测试对于确保您的应用保持其高性能标准至关重要。


原文:https://medium.com/@panuj330/pushing-flutter-to-the-limit-per...

你可能感兴趣的:(前端flutter)