Flutter 你的列表根本没有加载到我的心趴上

1,405 阅读6分钟

相关阅读

关注微信公众号 糖果代码铺 ,获取 Flutter 最新动态。

前言

有些感慨,现在群里也会不断有新人,会问一些简单的问题。大家都是从萌新过来的,当然问问题也要注意方式,如何问问题也是一门艺术。然后老油条们也应该更友善一些,多给新人一些建议。有新人肯定是好事,至少 flutter 还没有凉嘛。

最近有同学,在使用 pub-web.flutter-io.cn/packages/lo… 的时候会有一些疑问。

6 年前,从微软的 ISupportIncrementalLoading 中得到灵感,创造了属于 Flutter 平台的加载更多组件 pub-web.flutter-io.cn/packages/lo… 。这个组件一直都是蛮稳定的,所以介绍的文章就比较少了,最近(8个月前)新增了一些功能,随带就一起讲讲吧。

使用

为了照顾一下第一次使用这个组件的同学,我下面还是简单的介绍一下这个组件。

UI 和 数据 之间的契约

LoadingMoreBase 是组件提供的一个基类,用来给用户加载实际数据使用的。

实际开发中,你只需要继承它,并且实现 loadData 方法即可。当然你也需要关注 hasMore 这个属性,通过它来告诉组件是否还有更多的数据。

class TuChongRepository extends LoadingMoreBase<TuChongItem> {
  TuChongRepository({this.maxLength = 300});
  int _pageIndex = 1;
  bool _hasMore = true;
  @override
  bool get hasMore => _hasMore && length < maxLength;
  final int maxLength;

  @override
  Future<bool> refresh([bool notifyStateChanged = false]) async {
    _hasMore = true;
    _pageIndex = 1;
    final bool result = await super.refresh(true);
    return result;
  }

  @override
  Future<bool> loadData([bool isLoadMoreAction = false]) async {
 
    bool isSuccess = false;
    try {
      // 从服务端加载数据 feedList
      for (final TuChongItem item in feedList!) {
        if (item.hasImage && !contains(item) && hasMore) {
          add(item);
        }
      }

      _hasMore = feedList.isNotEmpty;
      _pageIndex++;
      isSuccess = true;
    } catch (exception, stack) {
      isSuccess = false;
      print(exception);
      print(stack);
    }
    return isSuccess;
  }
}

怎么创建一个加载更多列表

最简单一个加载更多 UI 代码如下图。

    LoadingMoreList(
      ListConfig<TuChongItem>(
        itemBuilder: ItemBuilder.itemBuilder,
        sourceList: listSourceRepository,
      ),
    ),

当然我们也支持其他列表:

  • GridView
    LoadingMoreList(
      ListConfig<TuChongItem>(
        itemBuilder: ItemBuilder.itemBuilder,
        sourceList: listSourceRepository,
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 3.0,
          mainAxisSpacing: 3.0,
        ),
      ),
    ),
  • WaterfallFlow (瀑布流)
    LoadingMoreList(
      ListConfig<TuChongItem>(
        extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 5,
          mainAxisSpacing: 5,
        ),
        itemBuilder: _buildItem,
        sourceList: listSourceRepository,
      ),
    ),
  • Sliver 系列

通过使用 LoadingMoreCustomScrollView,你可以加载任意的 Sliver 列表。

   LoadingMoreCustomScrollView(
     slivers: <Widget>[
       SliverAppBar(
         pinned: true,
         title: Text("MultipleSliverDemo"),
       ),
       // SliverList
       LoadingMoreSliverList(SliverListConfig<TuChongItem>(
         itemBuilder: ItemBuilder.itemBuilder,
         sourceList: listSourceRepository,
       )),
       // SliverGrid
       LoadingMoreSliverList(
         SliverListConfig<TuChongItem>(
           itemBuilder: ItemBuilder.itemBuilder,
           sourceList: listSourceRepository1,
           gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
             crossAxisCount: 2,
             crossAxisSpacing: 3.0,
             mainAxisSpacing: 3.0,
           ),
         ),
       ),
       // SliverWaterfallFlow
       LoadingMoreSliverList(
         SliverListConfig<TuChongItem>(
           itemBuilder: buildWaterfallFlowItem,
           sourceList: listSourceRepository2,
           extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
             crossAxisCount: 2,
             crossAxisSpacing: 5,
             mainAxisSpacing: 5,
           ),
         ),
       ),
     ],
   ),

自定义状态效果

加载更多有多种状态,你可以通过自定义 indicatorBuilder 来自定义自己的状态效果。

enum IndicatorStatus {
  none,
  // 加载更多
  loadingMoreBusying,
  // 列表为空时候的第一次全屏加载
  fullScreenBusying,
  // 加载更多报错
  error,
  // 列表为空时候的第一次全屏加载报错
  fullScreenError,
  // 没有更多数据加载
  noMoreLoad,
  // 空列表
  empty
}
  LoadingMoreList(
    ListConfig<TuChongItem>(
      itemBuilder: ItemBuilder.itemBuilder,
      sourceList: listSourceRepository,
      indicatorBuilder: _buildIndicator,
      padding: EdgeInsets.all(0.0),
    ),
  ),

 
  Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
    //if your list is sliver list ,you should build sliver indicator for it
    //isSliver=true, when use it in sliver list
    bool isSliver = false;

    Widget widget;
    switch (status) {
      case IndicatorStatus.None:
        widget = Container(height: 0.0);
        break;
      case IndicatorStatus.LoadingMoreBusying:
        widget = Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Container(
              margin: EdgeInsets.only(right: 5.0),
              height: 15.0,
              width: 15.0,
              child: getIndicator(context),
            ),
            Text("正在加载...不要着急")
          ],
        );
        widget = _setbackground(false, widget, 35.0);
        break;
      case IndicatorStatus.FullScreenBusying:
        widget = Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Container(
              margin: EdgeInsets.only(right: 0.0),
              height: 30.0,
              width: 30.0,
              child: getIndicator(context),
            ),
            Text("正在加载...不要着急")
          ],
        );
        widget = _setbackground(true, widget, double.infinity);
        if (isSliver) {
          widget = SliverFillRemaining(
            child: widget,
          );
        } else {
          widget = CustomScrollView(
            slivers: <Widget>[
              SliverFillRemaining(
                child: widget,
              )
            ],
          );
        }
        break;
      case IndicatorStatus.Error:
        widget = Text(
          "好像出现了问题呢?",
        );
        widget = _setbackground(false, widget, 35.0);

        widget = GestureDetector(
          onTap: () {
            listSourceRepository.errorRefresh();
          },
          child: widget,
        );

        break;
      case IndicatorStatus.FullScreenError:
        widget = Text(
          "好像出现了问题呢?",
        );
        widget = _setbackground(true, widget, double.infinity);
        widget = GestureDetector(
          onTap: () {
            listSourceRepository.errorRefresh();
          },
          child: widget,
        );
        if (isSliver) {
          widget = SliverFillRemaining(
            child: widget,
          );
        } else {
          widget = CustomScrollView(
            slivers: <Widget>[
              SliverFillRemaining(
                child: widget,
              )
            ],
          );
        }
        break;
      case IndicatorStatus.NoMoreLoad:
        widget = Text("没有更多的了。。不要拖了");
        widget = _setbackground(false, widget, 35.0);
        break;
      case IndicatorStatus.Empty:
        widget = EmptyWidget(
          "这里是空气!",
        );
        widget = _setbackground(true, widget, double.infinity);
        if (isSliver) {
          widget = SliverToBoxAdapter(
            child: widget,
          );
        } else {
          widget = CustomScrollView(
            slivers: <Widget>[
              SliverFillRemaining(
                child: widget,
              )
            ],
          );
        }
        break;
    }
    return widget;
  }

新功能

支持 center

center 其实蛮有用处的,具体的原理可以查看:

juejin.cn/post/684490…

有的同学会问,为什么要支持这个,做什么呢,很简单,可以做个聊天列表,向上翻的时候就是加载更多的历史数据。

chatList.gif

  • 向上滚动的列表即历史数据,滚动到顶部会自动加载更多的历史数据。
  • 正向的列表就是加载当前获取到的聊天数据。
    return LoadingMoreCustomScrollView(
      showGlowLeading: false,
      center: _centerKey,
      slivers: <Widget>[
        // 历史数据
        LoadingMoreSliverList<TuChongItem>(
          SliverListConfig<TuChongItem>(
            itemBuilder: itemBuilder,
            sourceList: listSourceRepository1,
          ),
        ),
        // 当前数据
        LoadingMoreSliverList<TuChongItem>(
          SliverListConfig<TuChongItem>(
            itemBuilder: itemBuilder,
            sourceList: listSourceRepository2,
          ),
          key: _centerKey,
        ),
      ],
    );

妈妈再也不会担心我不会写聊天列表了!

完整例子: github.com/fluttercand…

当然,也提供另外一种加载历史数据的方式,使用的是下拉刷新组件。这样你可以模拟出下拉回弹等动画。

    PullToRefreshNotification(
      onRefresh: onRefresh,
      maxDragOffset: 48,
      armedDragUpCancel: false,
      child: CustomScrollView(
        /// in case list is not full screen and remove ios Bouncing
        physics: const AlwaysScrollableClampingScrollPhysics(),
        controller: _scrollController,
        center: _centerKey,
        slivers: <Widget>[
          // 加载历史数据的效果
          PullToRefreshContainer(
            (PullToRefreshScrollNotificationInfo? info) {
              final double offset = info?.dragOffset ?? 0.0;
              //loading history data
              return SliverToBoxAdapter(
                child: Container(
                  height: offset,
                  alignment: Alignment.center,
                  child: const CupertinoActivityIndicator(color: Colors.blue),
                ),
              );
            },
          ),
          ExtendedSliverList(
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                final ChatItem item = newChats[index];
                return buildItem(item);
              },
              childCount: newChats.length,
            ),
            extendedListDelegate: const ExtendedListDelegate(),
          ),
          ExtendedSliverList(
            key: _centerKey,
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                final ChatItem item = chats[index];
                return buildItem(item);
              },
              childCount: chats.length,
            ),
            extendedListDelegate: const ExtendedListDelegate(),
          ),
        ],
      ),
    );

完整例子: github.com/fluttercand…

支持 LoadingMoreSliverList 封装处理

在实际使用中,经常有小伙伴会给 LoadingMoreSliverList 进行封装,比如

class MyLoadingMoreSliverList1 extends StatelessWidget {
  const MyLoadingMoreSliverList1({
    Key? key,
    required this.listSourceRepository,
  }) : super(key: key);

  final TuChongRepository listSourceRepository;
  @override
  Widget build(BuildContext context) {
    return SliverPadding(
      padding: const EdgeInsets.all(50),
      sliver: LoadingMoreSliverList<TuChongItem>(
        SliverListConfig<TuChongItem>(
          itemBuilder: itemBuilder,
          sourceList: listSourceRepository,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            crossAxisSpacing: 3.0,
            mainAxisSpacing: 3.0,
          ),
        ),
      ),
    );
  }
}

由于之前列表的 config 是通过 LoadingMoreCustomScrollViewslivers 判断是否是 LoadingMoreSliverList 获取的。封装就会导致组件没法获取对应的配置。

现在你只需要将 LoadingMoreCustomScrollView.getConfigFromSliverContext 设置成 true 即可。

    return LoadingMoreCustomScrollView(
      // support LoadingMoreCustomScrollView.slivers are not a direct LoadingMoreSliverList
      getConfigFromSliverContext: true,
      slivers: <Widget>[
        MyLoadingMoreSliverList1(
          listSourceRepository: listSourceRepository1,
        )
      ],
    );

支持 处理单次加载的内容

如果 LoadingMoreCustomScrollView 里面有 Sliver 只需要加载一次数据,我应该怎么写呢?

我想这应该也是实际开发会遇到的问题吧?新版本提供了 SliverLoadingDataSliveLoadingConfigLoadingMoreLoadingSliver 来加载一次性数据,当然也支持定义加载动画以及失败效果,你也可以增加错误重试点击效果。

  • 定义 数据 部分

class MySliverLoadingData extends SliverLoadingData<int> {
  @override
  Future<int?> onLoadData() async {
    await Future<void>.delayed(const Duration(seconds: 5));
    // retrun null means error
    return 123456;
  }
}

late MySliverLoadingData loadingData= MySliverLoadingData();
  • 定义 UI 部分
    return LoadingMoreCustomScrollView(
      slivers: <Widget>[
        LoadingMoreLoadingSliver<int>(
          SliveLoadingConfig<int>(
            builder: (BuildContext context, int? data) {
              return SliverToBoxAdapter(
                child: Container(
                  alignment: Alignment.center,
                  child: Text('Loading Data$data'),
                  color: Colors.blue,
                  height: 50.0,
                ),
              );
            },
            loadingData: loadingData,
          ),
        ),
      ],
    );

结语

pub-web.flutter-io.cn/packages/lo… 结合 pub-web.flutter-io.cn/packages/pu… 你可以做出任何效果的下拉刷新+列表加载更多的效果。

组件没有花时间去做一些非常炫酷效果,是因为当每个人拿到三方组件的时候,都应该对其做一定的封装,来满足各自公司的设计效果。不是不想做,而是希望组件尽量简单,可扩展性高。

法同学: 以前在 github 上开源代码,用户跟我说加个功能吧,我都会说好好好,但是时间一久也没想起来做。其实这样挺不好的。 现在用户跟我说加个功能吧,除非用户的建议真的很好到我想马上加这个功能的程度,否则我就会在 issue 直接说 as design ,抱歉我不想加,然后直接关闭了。作为一个有讨好倾向的人,这是我锻炼真诚和勇气的方式。

IMG_20250413_205130.png

最后想说,任何东西的设计都没法完全满足全部人的需求。对的,你的列表根本没有加载到我的心趴上。

Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群

最最后放上 Flutter Candies 全家桶,真香。