Flutter 复杂列表开发与性能优化全攻略(现在看为时不晚!)

2,179 阅读12分钟

​ ​微信公众号:小武码码码

一、Flutter 中常见的复杂列表样式及应用场景

在移动应用开发中,列表可以说是最常见、最重要的 UI 组件之一。它不仅能够高效地展示大量数据,还能提供丰富的交互方式,让用户能够快速浏览和查找所需信息。而在实际开发中,我们经常会遇到各种复杂的列表需求,这就对我们的开发能力提出了更高的要求。

在 Flutter 中,我们可以使用 ListViewGridView 等 Widget 来实现各种列表样式。下面,我就来介绍一些常见的复杂列表样式及其应用场景。

  1. 图文混排列表:这种列表的每个 Item 中既有图片,又有文字,布局样式丰富多样。比如,新闻资讯类应用中,每条新闻都会包含标题、摘要、配图等元素,展示形式吸引人、信息量大。
class NewsItem extends StatelessWidget {
  final String title;
  final String summary;
  final String imageUrl;

  NewsItem({this.title, this.summary, this.imageUrl});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          Image.network(imageUrl),
          ListTile(
            title: Text(title),
            subtitle: Text(summary),
          ),
        ],
      ),
    );
  }
}
  1. 多栏网格列表:与普通的单列列表不同,多栏列表可以在水平方向上展示多个 Item,充分利用屏幕空间。比如,电商应用中的商品列表,通常会采用两栏或三栏的网格布局,突出商品图片和价格。
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2, // 每行显示两个 Item
    childAspectRatio: 0.8, // Item 宽高比为 0.8
  ),
  itemBuilder: (context, index) {
    return ProductItem(product: products[index]);
  },
  itemCount: products.length,
)
  1. 分组列表:当数据具有明确的分类属性时,我们可以使用分组列表来展示。每个分组都有一个头部,用于显示该组的标题或概要信息。常见的应用场景包括通讯录、设置菜单等。
ListView.builder(
  itemBuilder: (context, index) {
    if (index == 0 || index == contacts.length + 1) {
      // 渲染分组头部
      String headerText = index == 0 ? '星标联系人' : '普通联系人';
      return ListTile(title: Text(headerText));
    } else {
      // 渲染联系人 Item
      int contactIndex = index - 1;
      Contact contact = contacts[contactIndex];
      return ContactItem(contact: contact);
    }
  },
  itemCount: contacts.length + 2, // 包括两个分组头部
)
  1. 展开收起列表:当每个 Item 的内容比较多时,我们可以使用展开收起列表来节省空间。点击 Item 可以展开详情,再次点击则收起。这种列表常用于 FAQ、博客评论等场景。
class ExpandableItem extends StatefulWidget {
  final String title;
  final String content;

  ExpandableItem({this.title, this.content});

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

class _ExpandableItemState extends State<ExpandableItem> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return ExpansionTile(
      title: Text(widget.title),
      children: [
        Text(widget.content),
      ],
      onExpansionChanged: (expanded) {
        setState(() {
          _isExpanded = expanded;
        });
      },
      initiallyExpanded: _isExpanded,
    );
  }
}
  1. 聊天气泡列表:在即时通讯应用中,聊天气泡是最常见的 UI 元素。它需要根据消息的发送方和接收方,显示不同的气泡样式和位置。同时,还要支持文本、图片、语音等多种消息类型。
class ChatBubble extends StatelessWidget {
  final String text;
  final bool isMe;

  ChatBubble({this.text, this.isMe});

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        padding: EdgeInsets.all(10),
        decoration: BoxDecoration(
          color: isMe ? Colors.blue : Colors.grey[300],
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(isMe ? 20 : 0),
            topRight: Radius.circular(isMe ? 0 : 20),
            bottomLeft: Radius.circular(20),
            bottomRight: Radius.circular(20),
          ),
        ),
        child: Text(text),
      ),
    );
  }
}

以上只是 Flutter 中复杂列表样式的冰山一角,在实际开发中,我们还会遇到更多个性化的需求。但无论列表的样式如何变化,其底层实现原理都是相通的。接下来,我们就来探讨一下 Flutter 中复杂列表的几种开发方式。

二、Flutter 中复杂列表的几种开发方式

在 Flutter 中,我们主要有以下几种方式来实现复杂列表:

  1. ListView:这是最基本、最常用的列表组件。它支持垂直和水平两个方向上的滚动,可以通过 builder 构造函数动态创建列表 Item。ListView 适用于 Item 数量不太多、页面结构相对简单的场景。
ListView.builder(
  itemBuilder: (context, index) {
    return ItemWidget(data: dataList[index]);
  },
  itemCount: dataList.length,
)
  1. GridView:用于实现网格列表,可以在水平和垂直方向上显示多个 Item。GridView 通过 SliverGridDelegate 来控制网格的布局,包括每行的 Item 数量、Item 的宽高比等。与 ListView 类似,GridView 也提供了 builder 构造函数用于动态创建 Item。
GridView.builder(
  	gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
	mainAxisSpacing: 10.0,
	crossAxisSpacing: 10.0,
	childAspectRatio: 1.0,
	),
	itemBuilder: (context, index) {
		return ItemWidget(data: dataList[index]);
		},
	itemCount: dataList.length,
)    
  1. CustomScrollView + Sliver:当我们需要实现更加复杂、灵活的列表布局时,就需要使用 CustomScrollView 和 Sliver 家族的组件了。CustomScrollView 可以包含多个 Sliver,每个 Sliver 负责渲染列表的一部分。通过组合不同的 Sliver,我们可以实现非常个性化的列表样式,如吸顶头部、嵌套列表、渐变背景等。
CustomScrollView(
  slivers: [
    SliverAppBar(
      title: Text('嵌套列表示例'),
      pinned: true,
    ),
    SliverGrid(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 1.0,
      ),
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return ItemWidget(data: gridDataList[index]);
        },
        childCount: gridDataList.length,
      ),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return ItemWidget(data: listDataList[index]);
        },
        childCount: listDataList.length,
      ),
    ),
  ],
)
  1. 第三方插件:除了 Flutter SDK 内置的列表组件外,我们还可以利用第三方插件来实现一些特殊的列表效果。比如,flutter_staggered_grid_view 可以实现瀑布流布局,sticky_headers 可以实现列表分组和吸顶头部等。这些插件可以帮助我们快速实现复杂的列表需求,提高开发效率。
// 使用 flutter_staggered_grid_view 实现瀑布流布局
StaggeredGridView.countBuilder(
  crossAxisCount: 4,
  itemCount: dataList.length,
  itemBuilder: (context, index) {
    return ItemWidget(data: dataList[index]);
  },
  staggeredTileBuilder: (index) {
    return StaggeredTile.count(2, index.isEven ? 2 : 1);
  },
  mainAxisSpacing: 8.0,
  crossAxisSpacing: 8.0,
)

以上就是 Flutter 中实现复杂列表的几种主要方式。在实际开发中,我们需要根据具体的需求来选择合适的方案。同时,还要注意列表的性能优化,尤其是在大量数据的情况下。接下来,我们就来重点讨论一下 Flutter 复杂列表的几个性能优化策略。

三、Flutter 复杂列表的高度测量和自适应优化

在 Flutter 中实现复杂列表时,我们经常会遇到一个棘手的问题:列表 Item 的高度不固定,需要根据内容自适应。这就需要我们在布局之前先测量每个 Item 的高度,然后再动态更新列表。如果处理不当,很容易引起性能问题,导致列表滑动卡顿。

下面,我就来分享几种常用的 Item 高度测量和自适应优化方法。

  1. 使用 Expanded 和 Flexible 实现 Item 高度自适应:如果 Item 的内容比较简单,可以使用 Expanded 或 Flexible 来自动撑开 Item 的高度。它们都可以让子组件填充父组件的剩余空间,区别在于 Expanded 必须填充完整的剩余空间,而 Flexible 可以根据需要填充部分空间。
ListView.builder(
  itemBuilder: (context, index) {
    return Row(
      children: [
        Expanded(
          child: Text('标题 ${index + 1}'),
        ),
        Flexible(
          child: Text('这是一段很长很长的内容...'),
        ),
      ],
    );
  },
  itemCount: dataList.length,
)
  1. 使用 IntrinsicHeight 实现 Item 高度自适应:IntrinsicHeight 可以根据子组件的内容自动调整父组件的高度。它会在布局前先测量子组件的最大高度,然后将父组件的高度设置为这个最大值。使用 IntrinsicHeight 可以方便地实现 Item 高度自适应,但要注意它可能会导致额外的测量开销。
ListView.builder(
  itemBuilder: (context, index) {
    return IntrinsicHeight(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('标题 ${index + 1}'),
          Text('这是一段很长很长的内容...'),
        ],
      ),
    );
  },
  itemCount: dataList.length,
)
  1. 使用 CustomMultiChildLayout 实现 Item 高度自适应:如果 Item 的布局非常复杂,包含多个子组件,且子组件的大小依赖于彼此,那么可以考虑使用 CustomMultiChildLayout。它允许我们完全自定义子组件的布局逻辑,手动测量和设置每个子组件的位置和大小。但这也意味着我们需要编写更多的代码来处理布局细节。
class AdaptiveLayout extends MultiChildLayoutDelegate {
  @override
  void performLayout(Size size) {
    Size leadingSize = layoutChild(
      childId: 'leading',
      constraints: BoxConstraints.loose(size),
    );

    double remainingWidth = size.width - leadingSize.width;
    Size trailingSize = layoutChild(
      childId: 'trailing',
      constraints: BoxConstraints.loose(Size(remainingWidth, size.height)),
    );

    positionChild('leading', Offset.zero);
    positionChild(
      'trailing',
      Offset(leadingSize.width, (size.height - trailingSize.height) / 2),
    );
  }

  @override
  bool shouldRelayout(AdaptiveLayout oldDelegate) => false;
}

CustomMultiChildLayout(
  delegate: AdaptiveLayout(),
  children: [
    LayoutId(
      id: 'leading',
      child: Text('标题'),
    ),
    LayoutId(
      id: 'trailing',
      child: Text('这是一段很长很长的内容...'),
    ),
  ],
)
  1. 使用 LayoutBuilder 实现 Item 高度自适应:LayoutBuilder 可以在布局过程中动态获取父组件的约束信息,并根据这些信息来调整子组件的布局。利用 LayoutBuilder,我们可以方便地实现 Item高度自适应,而且不会引入额外的测量开销。 
ListView.builder(
  itemBuilder: (context, index) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Row(
          children: [
            Container(
              width: constraints.maxWidth / 2,
              child: Text('标题 ${index + 1}'),
            ),
            Container(
              width: constraints.maxWidth / 2,
              child: Text('这是一段很长很长的内容...'),
            ),
          ],
        );
      },
    );
  },
  itemCount: dataList.length,
)
  1. 使用 CustomScrollView + SliverList 实现 Item 高度自适应:前面我们介绍过,CustomScrollView 可以通过组合不同的 Sliver 来实现复杂的列表布局。而 SliverList 恰好支持 Item 高度自适应,它会在布局时自动调整每个 Item 的高度。
CustomScrollView(
  slivers: [
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return Row(
            children: [
              Text('标题 ${index + 1}'),
              Text('这是一段很长很长的内容...'),
            ],
          );
        },
        childCount: dataList.length,
      ),
    ),
  ],
)

以上就是几种常用的 Item 高度自适应优化方法。在实际开发中,我们需要根据 Item 的复杂度和性能要求,选择合适的方案。对于简单的 Item,使用 ExpandedFlexible 或 IntrinsicHeight 就可以满足需求;对于复杂的 Item,可以考虑使用 CustomMultiChildLayoutLayoutBuilder 或 SliverList 来手动控制布局和测量。

需要注意的是,过度使用 IntrinsicHeight 和 CustomMultiChildLayout 可能会导致性能下降,因为它们都需要额外的测量过程。因此,在使用这些方法时,要权衡好性能和灵活性之间的平衡。

四、Flutter 复杂列表的性能优化策略

除了 Item 高度自适应外,Flutter 复杂列表还有一些其他的性能优化策略。下面,我就来介绍几个常见的优化方向。

  1. 懒加载:当列表数据量很大时,如果一次性加载全部数据,会占用大量内存,导致页面卡顿甚至崩溃。这时,我们可以使用懒加载策略,即只加载当前可见区域的数据,等用户滑动列表时再动态加载其他数据。Flutter 中的 ListView.builder 和 GridView.builder 都支持懒加载,它们会根据 itemCount 参数自动判断是否需要创建新的 Item。
// 初始只加载前 20 条数据
int _itemCount = 20;

ListView.builder(
  itemBuilder: (context, index) {
    // 当滑动到底部时,动态加载更多数据
    if (index == _itemCount - 1) {
      _loadMoreData();
    }
    return ItemWidget(data: dataList[index]);
  },
  itemCount: _itemCount,
)

void _loadMoreData() {
  // 模拟异步加载数据
  Future.delayed(Duration(seconds: 1), () {
    setState(() {
      _itemCount += 10;
    });
  });
}
  1. 缓存 Item:在列表滑动过程中,如果每次都重新创建和销毁 Item,会产生大量的内存分配和垃圾回收,从而影响性能。为了避免这种情况,我们可以缓存已经创建的 Item,在滑出屏幕后不立即销毁,而是保存在内存中,等下次需要时再重新利用。Flutter 中的 ListView.builder 和 GridView.builder 默认会缓存一定数量的 Item,我们也可以通过 cacheExtent 参数来手动设置缓存区域的大小。
ListView.builder(
  itemBuilder: (context, index) {
    return ItemWidget(data: dataList[index]);
  },
  itemCount: dataList.length,
  cacheExtent: 200.0, // 设置缓存区域为200像素
)
  1. 减少重绘:频繁的重绘会导致界面闪烁,影响用户体验。为了减少不必要的重绘,我们可以利用 RepaintBoundary 组件将需要重绘的部分与其他部分隔离开来。RepaintBoundary 会在其子组件重绘时创建一个独立的绘制层,避免影响其他组件。另外,我们还可以通过 const 关键字来标记不变的组件,告诉 Flutter 可以直接复用之前的渲染结果。
ListView.builder(
  itemBuilder: (context, index) {
    // 使用 RepaintBoundary 隔离 Item
    return RepaintBoundary(
      child: ItemWidget(data: dataList[index]),
    );
  },
  itemCount: dataList.length,
)

// 使用 const 标记不变的组件
class ItemWidget extends StatelessWidget {
  final String data;

  const ItemWidget({Key key, this.data}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Text('不变的文本');
  }
}
  1. 避免深层嵌套:在构建复杂列表时,我们经常需要嵌套多层组件,比如 ListView 里面包含 Row,Row 里面又包含 Column 等。但是,过度的嵌套会导致布局树变得非常深,增加测量和渲染的开销。因此,我们要尽量避免不必要的嵌套,可以使用 FlexWrap 等组件来减少嵌套层级。
// 避免嵌套的写法
ListView.builder(
  itemBuilder: (context, index) {
    return Row(
      children: [
        Column(
          children: [
            Text('标题'),
            Text('子标题'),
          ],
        ),
        Text('内容'),
      ],
    );
  },
)

// 优化后的写法
ListView.builder(
  itemBuilder: (context, index) {
    return Flex(
      direction: Axis.horizontal,
      children: [
        Text('标题'),
        Text('子标题'),
        Text('内容'),
      ],
    );
  },
)

以上就是几个常见的 Flutter 复杂列表性能优化策略。在实际开发中,我们还需要根据具体情况来选择和组合不同的优化方法。比如,可以结合懒加载和缓存 Item 来优化长列表,结合 RepaintBoundaryconst来优化频繁重绘的列表,结合FlexWrap 来优化深层嵌套的列表等。

总之,Flutter 复杂列表的性能优化是一个综合性的工程,需要从多个角度来考虑和权衡。除了上面提到的方法外,我们还要注意以下几点:

  1. 尽量使用 Flutter SDK 内置的列表组件,如 ListView 和 GridView,它们在性能上已经做了很多优化。
  2. 合理使用 StatefulWidget 和 StatelessWidget,避免不必要的状态更新和重建。
  3. 避免在列表滑动时执行耗时操作,如网络请求、复杂计算等,可以使用异步或缓存来优化。
  4. 使用 DevTools 等工具来分析和定位性能瓶颈,如 GPU 渲染时间、CPU 使用率等。

五、Flutter 列表与原生列表的异同

最后,我们来简单对比一下 Flutter 列表与原生列表的异同。

相同点:

  • 都支持垂直和水平方向上的滚动。
  • 都支持下拉刷新和上拉加载等交互操作。
  • 都需要考虑列表的性能优化,如懒加载、缓存等。

不同点:

  • 实现方式不同:Flutter 列表是基于 Widget 树来实现的,而原生列表是基于原生 View 体系。
  • 性能特点不同:Flutter 列表在滑动和渲染上可能不如原生列表流畅,但在页面切换和自定义 UI 上有优势。
  • 开发成本不同:Flutter 列表的开发成本相对较低,因为它可以跨平台复用代码,而原生列表需要分别为 Android 和 iOS 编写实现。

        总的来说,Flutter 列表与原生列表各有优劣。Flutter 列表更加灵活和高效,适合实现一些复杂和个性化的列表样式;而原生列表在性能和体验上更有优势,适合实现一些对流畅度要求较高的场景。

        作为开发者,我们需要根据实际需求来选择合适的技术方案。如果应用的核心功能是基于列表的,并且对性能有较高要求,那么可以考虑使用原生列表;如果应用的列表样式比较复杂多变,并且需要快速迭代和跨平台复用,那么可以考虑使用 Flutter 列表。

总结

        回顾全文,我们深入探讨了 Flutter 复杂列表的方方面面,包括常见的样式和场景、几种主要的实现方式、高度测量和自适应优化、性能优化策略,以及与原生列表的异同。可以看到,Flutter 列表的开发和优化是一个相当复杂和有挑战性的过程,需要我们掌握多方面的知识和技巧。

        但是,只要我们勤于学习、善于思考、勇于实践,就一定能够驾驭 Flutter 列表的开发,创造出优秀的用户体验。作为 Flutter 开发者,我也将继续钻研和分享,与大家一起进步。

        以上就是我对 Flutter 复杂列表的一些理解和经验,希望对大家有所帮助。如果您有任何问题或建议,欢迎随时交流探讨。让我们携手共进,一起打造出更加优秀的 Flutter 应用!