Riverpod之列表封装

3,307 阅读2分钟

本文就是一个简单的学习Riverpod的Demo,然后做了些常用列表界面的通用性封装,如果大家发现有什么错误多谢指正哈👻

提前准备

使用了jsonplaceholder mock 数据

数据model

import 'package:freezed_annotation/freezed_annotation.dart';

//http://jsonplaceholder.typicode.com/photos?_start=101&_limit=5
part 'product_model.freezed.dart';
part 'product_model.g.dart';

@freezed
class ProductModel with _$ProductModel {
  factory ProductModel({
    int? albumId,
    int? id,
    String? title,
    String? url,
    String? thumbnailUrl,
  }) = _ProductModel;

  factory ProductModel.fromJson(Map<String, dynamic> json) =>
      _$ProductModelFromJson(json);
}

界面状态

image.png

参考一般的列表界面,状态可以分为:

  • loading:第一次进来loading状态
  • ready:加载成功,显示数据
  • empty:数据为空,显示空界面
  • error:错误界面,一般会带有刷新按钮

抽离State

import 'package:freezed_annotation/freezed_annotation.dart';

part 'list_view_state.freezed.dart';

@freezed
class ListViewState<T> with _$ListViewState<T> {
  const factory ListViewState.loading() = Loading;
  const factory ListViewState.empty() = Empty;
  const factory ListViewState.ready(T data) = Ready<T>;
  const factory ListViewState.error({required String error}) = Error;
}

列表Notifier封装

ListViewStateNotifier列表加载的通用封装

import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'list_view_state.dart';

typedef ToastFunction = void Function(String error, {Error? e});

/*
1. 首次进来显示loadding,第二次就系那是refresh的动画
2. 错误也是首次进来发生错误才显示,第二次就toast
*/
class ListViewStateNotifier<T> extends StateNotifier<ListViewState<List<T>>> {
  ListViewStateNotifier({
    required this.fetchItems,
    this.pageSize = 10,
    this.pageIndex = 1,
  }) : super(const ListViewState.loading()) {
    init();
  }
  final EasyRefreshController _refreshController = EasyRefreshController();

  EasyRefreshController get refreshController => _refreshController;

  final Future<List<T>> Function(int pageIndex, int pageSize) fetchItems;

  int _page = 0;
  int pageSize = 10;
  int pageIndex = 1;
  List<T> _items = [];

  void init() {
    firstLoadPage();
  }

  Future<void> firstLoadPage() async {
    _page = pageIndex;
    try {
      final List<T> list = await fetchItems(_page, pageSize);
      if (list.isEmpty) {
        state = const ListViewState.empty();
      } else {
        _items = list;
        state = ListViewState.ready(list);
      }
      _refreshController.finishRefresh(
          success: true, noMore: list.length < pageSize);
      _refreshController.resetLoadState();
      _page += 1;
    } catch (e) {
      state = ListViewState.error(error: e.toString());
    }
  }

  Future<void> refreshData({ToastFunction? fnToast}) async {
    _page = pageIndex;
    try {
      final List<T> list = await fetchItems(_page, pageSize);
      if (list.isEmpty) {
        state = const ListViewState.empty();
      } else {
        _items = list;
        // state = ListViewState.ready(list);
        state = const ListViewState.error(error: "发生错误啦");
      }
      _refreshController.finishRefresh(
          success: true, noMore: list.length < pageSize);
      _refreshController.resetLoadState();
      _page += 1;
    } catch (e) {
      _refreshController.finishRefresh(success: false);
      if (fnToast != null) {
        fnToast(e.toString());
      }
      // state = ListViewState.error(error: e.toString());
    }
  }

  Future<void> loadMore({ToastFunction? fnToast}) async {
    try {
      final List<T> list = await fetchItems(_page, pageSize);
      if (list.isNotEmpty) {
        _items.addAll(list);
        state = ListViewState.ready(_items);
      }
      _refreshController.finishLoad(
          success: true, noMore: list.length < pageSize);
      _page += 1;
    } catch (e) {
      _refreshController.finishLoad(success: false);
      // state = ListViewState.error(error: e.toString());
      if (fnToast != null) {
        fnToast(e.toString());
      }
    }
  }

  @override
  void dispose() {
    _refreshController.dispose();
    super.dispose();
  }
}

下拉刷新和上拉加载更多发生错误的时候需要toast,这块暴露了fnToast一个参数方便界面处理

Provider

productListProvider做界面的逻辑处理

final productListProvider = StateNotifierProvider<
    ListViewStateNotifier<ProductModel>,
    ListViewState<List<ProductModel>>>((ref) {
  final api = ref.watch(apiService);
  return ListViewStateNotifier<ProductModel>(
      pageIndex: 1,
      pageSize: 10,
      fetchItems: (int pageIndex, int pageSize) {
        return api.fetchProducts(pageIndex: pageIndex, pageSize: pageSize);
      });
});

UI显示

UI显示会非常清爽,再也不用各种setState了

Consumer(
        builder: (context, ref, child) {
          final state = ref.watch(productListProvider);
          return state.when(
            empty: () => const Text(
              'empty',
              style: TextStyle(
                color: Colors.black,
                fontWeight: FontWeight.normal,
                fontSize: 14,
              ),
              textAlign: TextAlign.center,
              overflow: TextOverflow.ellipsis,
              maxLines: 1,
            ),
            ready: (data) => _buildContent(context, ref, data),
            error: (String error) {
              return Center(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(error),
                    ),
                    TextButton(
                      child: const Text("重试下"),
                      onPressed: () {
                        ref.refresh(productListProvider);
                      },
                    ),
                  ],
                ),
              );
            },
            loading: () => const Center(child: Text("loading..")),
          );
        },
      )

总结

项目地址

使用了Riverpod,感觉就是逻辑和UI更独立了,UI处理UI的体验,逻辑处理数据,非常灵活,个人感觉哈。

参考资料