分享一下结合getx实现瀑布流

来源:6-6 基于StaggeredGridView封装首页双Feed列表

weixin_慕神9322259

2025-05-07

跟着老师的课一步步写代码,但是想着能更有效的学习,于是自己引入了getx状态管理库做一些魔改。
选择getx的原因,我喜欢getx这种把状态都放在controller里,然后在stateless widget里就可以专注UI部分(getx可以不需要statefull widget),状态和UI两块分开,代码就比较清晰明了,而且getx还提供route和依赖注入的功能,我的背景是后端开发,所以我比较喜欢这种做法。

这里分享一下自己的基于getx的瀑布流解决方案。
首先,让我们对数据做一下抽象,本质上我们需要一个可分页的数据,使用getx,我们可以把分页的数据都塞在controller里,然后对于数据的获取和重置,都写成方法,非常易懂。

abstract class BasePagedController<T> extends GetxController {
  final RxList<T> data = <T>[].obs;
  final RxInt currentPage = 1.obs;
  final RxBool isLoading = false.obs;
  final RxBool hasMore = true.obs;
  final int pageSize = 10;

  Future<List<T>> loadData(int page, int pageSize);

  @override
  void onInit() {
    super.onInit();
    loadMore();
  }

  Future<void> loadMore() async {
    if (isLoading.value || !hasMore.value) return;

    isLoading.value = true;
    print("loading more... currentPage: $currentPage");
    try {
      final newItems = await loadData(currentPage.value, pageSize);
      if (newItems.isEmpty) {
        hasMore.value = false;
      } else {
        data.addAll(newItems);
        currentPage.value++;
      }
    } catch (e) {
      Get.snackbar('错误', '加载数据失败: $e');
    } finally {
      isLoading.value = false;
    }
  }

  Future<void> refreshData() async {
    currentPage.value = 1;
    hasMore.value = true;
    data.clear();
    await loadMore();
  }

}

对于video的瀑布流,我们需要的就是能够获取分页的video model,所以继承一下上面的抽象类,实现一下loadData即可

class VideoPagedController extends BasePagedController<VideoModel> {

  final String categoryName;

  VideoPagedController({required this.categoryName});

  @override
  Future<List<VideoModel>> loadData(int page, int pageSize) async {
    final HomeModel homeData = await HomeDao.get(
        categoryName,
        pageIndex: page,
        pageSize: pageSize
    );

    return homeData.videoList?? [];
  }
}

数据状态的部分完成了,现在我们搞一个瀑布流页面。

class CommonWaterfallGridView<T> extends StatelessWidget {
  final BasePagedController<T> controller;
  final Widget Function(T item) itemBuilder;
  final int crossAxisCount;
  final double crossAxisSpacing;
  final double mainAxisSpacing;
  final Widget? banner; // banner作为可选
  const CommonWaterfallGridView({
    super.key,
    required this.controller,
    required this.itemBuilder,
    this.crossAxisCount = 2,
    this.crossAxisSpacing = 4,
    this.mainAxisSpacing = 4,
    this.banner,
  });

  @override
  Widget build(BuildContext context) {
    return Obx(() {
      return NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scrollInfo) {
          // 判断是否滑动到底部
          if (scrollInfo.metrics.pixels ==
              scrollInfo.metrics.maxScrollExtent) {
            controller.loadMore();
          }
          return false;
        },
        child: CustomScrollView(
          slivers: [
            // 顶部Banner
            if (banner != null) SliverPadding(padding: EdgeInsets.only(top: 5), sliver: SliverToBoxAdapter(child: banner),),

            // 瀑布流主体
            SliverPadding(
              padding: EdgeInsets.only(top: 10),
              sliver: SliverMasonryGrid.count(
                crossAxisCount: crossAxisCount,
                mainAxisSpacing: mainAxisSpacing,
                crossAxisSpacing: crossAxisSpacing,
                childCount: controller.data.length,
                itemBuilder: (context, index) {
                  return itemBuilder(controller.data[index]);
                },
              ),
            ),

            // 底部加载指示器
            SliverToBoxAdapter(
              child: controller.hasMore.value
                  ? _defaultLoadingIndicator()
                  : const SizedBox(), // TODO: 如果没有数据了,做一个提示? 可以作为field,让调用方自己决定
            ),
          ],
        ),
      );
    });
  }

  Widget _defaultLoadingIndicator() {
    return const Padding(
      padding: EdgeInsets.all(16.0),
      child: Center(child: CircularProgressIndicator()),
    );
  }
}

回到我们的home_tab_page,只需要复用CommonWaterfallGridView即可,这里代码就很简单了。

class HomeTabPage extends StatelessWidget {
  final String name;
  final List<BannerModel>? bannerList;

  const HomeTabPage({super.key, required this.name, this.bannerList});

  @override
  Widget build(BuildContext context) {
    final VideoPagedController videoPagedController = Get.put(VideoPagedController(categoryName: name));
    
    return CommonWaterfallGridView(
      controller: videoPagedController,
      banner: _banner(),
      itemBuilder: (videoModel) => VideoCard(videoMo: videoModel),
    );
  }

  _banner() => Padding(
    padding: EdgeInsets.only(left: 8, right: 8),
    child: HiBanner(bannerList!),
  );
}

我看了一些状态管理库,比如provider, getx, signals。我的感受是flutter在状态管理库这里,没有一个比较统一的解决方案,类似spring在java中的地位。所以对于初学者,该选择什么学习,开发项目,就挺迷茫的。不过想了想,有时候技术并不重要,我们到底要做一个什么样的App,这个App的功能是否价值才是最重要的,所以对于初学者纠结这么多干嘛呢,挑一个喜欢的库能实现需求就够了,技术选型这种终极问题,等真的遇到了实际问题再说吧

写回答

1回答

CrazyCodeBoy

2025-05-08

很棒。
想要用getx做状态管理的小伙伴,也可以看下这个课程
https://coding.imooc.com/class/741.html
0
0

Flutter高级进阶实战-仿哔哩哔哩-掌握Flutter高阶技能

一次性掌握Flutter高阶技能+商业级复杂项目架构设计与开发方案

1793 学习 · 900 问题

查看课程