Flutter 使用bloc模拟首页实现下拉刷新

目录

  • 前言
  • 一、项目结构
  • 二、示例演示
    • 1. 定义 Repository
    • 2. 实现banner状态管理与布局
    • 3. 实现文章列表状态管理与布局
    • 4. 具体使用布局, 放入main.dart中即可看到效果
  • 总结


前言

思考:这篇文章主要是记录bloc与下拉刷新功能的使用
本篇文章是延续上一篇文章中所包含的bloc和自定义http库的使用,使用发现在上一篇文章中对bloc的理解有些误区导致最后实现好几个方式都感觉不尽人意,最终尝试查看官方示例与个人示例进行比对后发现了问题所在, 下面是效果图: (注:所使用的api来源于wanandroid api)

flutter bloc与上下拉刷新使用记录演示


一、项目结构

├── bloc                                 - 业务逻辑
│   ├── banner                           - 轮播图
│   │   ├── bloc.dart
│   │   ├── contract.dart
│   │   └── view.dart
│   ├── homeList                         - 首页列表
│   │   ├── bloc.dart                    - 具体业务逻辑
│   │   ├── contract.dart                - 包含start与enevt
│   │   └── view.dart                    - 子布局
│   └── index.dart                       - 导出配置
└── view.dart                            - 布局

二、示例演示

1. 定义 Repository

‘est_repository.dart’:用于调用主页面对应接口

class TestRepository extends BaseRepository {
  final _repository = MyHttpRepository();

  /// 获取轮播图
  Future<RepositoryResult<List<BannerModel>>> getBanner({
    // 请求结束,可用于关闭加载动画
    void Function(bool isShow)? onLoading,
  }) async {
    final request = RockRequestBuilder(url: '/banner/json');
    final response = await _repository.baseRequest(
        onLoading: onLoading,
        request: () => RockNetHelp.get(request),
        onDataConversion: (jsonData) => BaseModel.fromJson(
            jsonData, (json) => BannerModel.fromJsonArray(json)));
    return response;
  }

  /// 获取首页列表
  Future<RepositoryResult<HomeArticleModel>> getArticleList(
    int page,
    int size, {
    // 请求结束,可用于关闭加载动画
    void Function(bool isShow)? onLoading,
  }) async {
    final request = RockRequestBuilder(url: '/article/list/$page/json')
        .addQuery('page_size', size);
    return await _repository.baseRequest(
        onLoading: onLoading,
        request: () => RockNetHelp.get(request),
        onDataConversion: (jsonData) => BaseModel.fromJson(
            jsonData, (json) => HomeArticleModel.fromJson(json)));
  }
}

‘my_http_repository.dart‘:请求成功逻辑失败的处理,并将http 401也放在了同一个地方处理

class MyHttpRepository extends HttpRepository {

  /// 请求失败处理, 独立个体方便使用, 上面代码也使用到了这个,这里的封装需要根据对应项目业务来展开
 static void requestFailureError(int errCode, String? errMsg, void Function()? onAuth) {
    if (errCode == 401 || errCode == -1001) {
      onAuth?.call();
    }
  }
}

2. 实现banner状态管理与布局

contract.dart:定义start与enevt

part of 'bloc.dart';

/// 状态
enum BannerStatus { init, success, noData, onAuthority }
final class BannerState extends Equatable {

  final BannerStatus status;
  final List<BannerModel> body;

  const BannerState({this.status = BannerStatus.init, this.body = const []});

  
  List<Object?> get props => [status, body];

  BannerState copyWith({
    BannerStatus? status,
    List<BannerModel>? body,
  }) {
    return BannerState(
      status: status ?? this.status,
      body: body ?? this.body,
    );
  }
}

// 事件
abstract class BannerEvent {

  /// 发送请求
  static void send(BuildContext context) {
    context.read<BannerBloc>()
        .add(BannerSend());
  }
}

/// 发送请求
final class BannerSend extends BannerEvent {}

bloc.dart:具体业务逻辑

part 'contract.dart';

/// 轮播图
class BannerBloc extends Bloc<BannerEvent, BannerState> {
  /// 网络请求
  final TestRepository _repository;

  BannerBloc({required TestRepository repository})
      : _repository = repository,
        super(const BannerState()) {
    on<BannerSend>(_initBannerSend);
  }

  void _initBannerSend(BannerSend event, Emitter<BannerState> emit) async {
    // http请求
    final response = await _repository.getBanner();
    // 判断当前请求结果
    switch (response) {
      case Success():
        // 返回加载成功的数据
        emit(state.copyWith(
          status: BannerStatus.success,
          body: response.success,
        ));
        break;
      case Error():
        final error = response.error;
        if (error case NoData()) {
          // 返回无数据
          emit(state.copyWith(status: BannerStatus.noData));
        }
        if (error case RequestFailure()) {
          // 在下面使用中有对这个函数的定义,这个函数对应于具体的业务项目的业务逻辑处理
          MyHttpRepository.requestFailureError(
            error.code,
            error.msg,
            () => emit(
              state.copyWith(status: BannerStatus.onAuthority),
            ),
          );
        }
        break;
    }
  }
}

view.dart:banner布局,使用到了card_swiper、cached_network_image

import 'bloc.dart';

/// 轮播图
class BannerViewWidget extends StatelessWidget {
  const BannerViewWidget({super.key});

  
  Widget build(BuildContext context) {
    return SliverToBoxAdapter(
      child: SizedBox(
        height: 200,
        child: BlocBuilder<BannerBloc, BannerState>(
          builder: (context, state) {
            if (state.status == BannerStatus.onAuthority) {
              // 跳转路由
            }
            if (state.status == BannerStatus.success) {
              final urlArray = state.body.map((m) => m.imagePath).toList();
              return _banner(urlArray);
            }
            return const Center();
          },
        ),
      ),
    );
  }

  /// 轮播图具体实现
  Widget _banner(List<String> urls) => BannerWidget(
        urls: urls,
        widget: (BuildContext context, int index, String url) => Padding(
          padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
          child: CachedNetworkImage(
            imageUrl: url,
            imageBuilder: (context, imageProvider) => Container(
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(10),
                //设置圆角
                image: DecorationImage(
                  image: imageProvider,
                  fit: BoxFit.fill,
                ),
              ),
            ),
          ),
        ),
      );
}

3. 实现文章列表状态管理与布局

contract.dart:定义start与enevt

part of 'bloc.dart';

// 状态
enum HomeListStatus { init, success, onAuthority }

final class HomeListState extends Equatable {
  final HomeListStatus status;

  /// 数据
  final HomeArticleModel? body;

  const HomeListState({this.status = HomeListStatus.init, this.body});

  
  List<Object?> get props => [status, body];

  HomeListState copyWith({
    HomeListStatus? status,
    HomeArticleModel? body,
  }) {
    return HomeListState(
      status: status ?? this.status,
      body: body ?? this.body,
    );
  }
}

/// 事件
abstract class HomeListEvent {
  /// 发送请求
  static void send(
    BuildContext context,
    RefreshController refreshController, {
    int page = 0,
    int pageSize = 10,
  }) {
    context
        .read<HomeListBloc>()
        .add(HomeListSend(refreshController, page, pageSize));
  }
}

final class HomeListSend extends HomeListEvent {
  final RefreshController refreshController;
  final int page;
  final int pageSize;

  HomeListSend(this.refreshController, this.page, this.pageSize);
}

bloc.dart:具体业务逻辑

part 'contract.dart';

class HomeListBloc extends Bloc<HomeListEvent, HomeListState> {
  /// 网络请求
  final TestRepository _repository;

  HomeListBloc({required TestRepository repository})
      : _repository = repository,
        super(const HomeListState()) {
    on<HomeListSend>(_initHomeListSend);
  }

  void _initHomeListSend(
      HomeListSend event, Emitter<HomeListState> emit) async {
    // http请求
    final response = await _repository.getArticleList(
      event.page,
      event.pageSize,
    );
    // 判断当前请求结果
    switch (response) {
      case Success():
      	final body = response.success;
        if (event.page < body.pageCount) {
          _refreshSuccess(event);

          // 返回加载成功的数据
          emit(state.copyWith(
            status: HomeListStatus.success,
            body: body,
          ));
        } else {
          _refreshNoData(event);
        }
        break;
      case Error():
        final error = response.error;
        switch (error) {
          case NoData():
            // 关闭刷新
            _refreshNoData(event);
            break;
          case RequestFailure():
            // 返回成功
            _refreshSuccess(event);
            // 在下面使用中有对这个函数的定义,这个函数对应于具体的业务项目的业务逻辑处理
            MyHttpRepository.requestFailureError(
              error.code,
              error.msg,
              () => emit(
                state.copyWith(status: HomeListStatus.onAuthority),
              ),
            );
            break;
          default:
            _refreshFailed(event);
            break;
        }
    }
  }

  /// 下拉刷新成功
  void _refreshSuccess(HomeListSend event) {
    if (event.page <= 0) {
      // 关闭下拉刷新
      event.refreshController.refreshCompleted(resetFooterState: true);
    } else {
      // 关闭上拉
      event.refreshController.loadComplete();
    }
  }

  /// 没有数据
  void _refreshNoData(HomeListSend event) {
    if (event.page <= 0) {
      // 关闭下拉刷新
      event.refreshController.refreshCompleted(resetFooterState: true);
    } else {
      // 关闭上拉
      event.refreshController.loadNoData();
      // Future.delayed(const Duration(seconds: 1), () {
      //   // 延迟关闭加载项
      //   event.refreshController.loadComplete();
      // });
    }
  }

  // 数据异常,下拉异常提示
  void _refreshFailed(HomeListSend event) {
    if (event.page <= 0) {
      event.refreshController.refreshFailed();
    } else {
      event.refreshController.loadFailed();
    }
  }
}

view.dart:list布局,使用到了pull_to_refresh

import 'bloc.dart';

class HomeListView extends StatelessWidget {
  /// item 数据
  final List<HomeArticleDataItemModel> itemData = [];

  HomeListView({super.key});

  
  Widget build(BuildContext context) {
    return BlocBuilder<HomeListBloc, HomeListState>(builder: (context, state) {
      if (state.status == HomeListStatus.onAuthority) {
        // 跳转路由
      }
      if (state.status == HomeListStatus.success) {
        state.body?.let((body) {
          // 判断是否需要清除数据
          if (body.offset <= 0) {
            itemData.clear();
          }
          itemData.addAll(body.itemArray);
        });
      }
      return _bodyWidget();
    });
  }

  // list
  Widget _bodyWidget() => SliverList(
          delegate: SliverChildBuilderDelegate(childCount: itemData.length,
              (BuildContext context, int index) {
        final model = itemData[index];
        return _listItem(index, model);
      }));

  /// item 布局
  Widget _listItem(int index, HomeArticleDataItemModel model) {
    return GestureDetector(
      onTap: () {
        LogUtil.debug('$index >>> ${model.title}');
      },
      child: Card(
        margin: const EdgeInsets.fromLTRB(10, 5, 10, 10),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(model.title),
              const SizedBox(height: 15),
              Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
                Text(model.author == '' ? model.shareUser : model.author),
                Text(model.niceDate)
              ])
            ],
          ),
        ),
      ),
    );
  }
}

4. 具体使用布局, 放入main.dart中即可看到效果

view.dart:定义具体页面

import 'bloc/index.dart';

/// 首页测试页面2
class HomeTwoPage extends StatelessWidget {
  final _repository = TestRepository();

  HomeTwoPage({super.key});

  
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(
          create: (BuildContext context) => BannerBloc(repository: _repository),
        ),
        BlocProvider(
          create: (BuildContext context) =>
              HomeListBloc(repository: _repository),
        ),
      ],
      child: const _HomePageView(),
    );
  }
}

class _HomePageView extends StatefulWidget {
  const _HomePageView();

  
  State<_HomePageView> createState() => _HomePageViewState();
}

class _HomePageViewState extends State<_HomePageView> {
  final RefreshController _refreshController =
      RefreshController(initialRefresh: true);
  int page = 1;

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => initAsyncState());
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text('测试首页'),
        ),
        body: _bodyWidget());
  }

  /// 页面内容
  Widget _bodyWidget() {
    return ColoredBox(
      color: Colors.white,
      child: _bodyRefresherWidget(),
    );
  }

  /// 下拉刷新控件
  Widget _bodyRefresherWidget() {
    return SmartRefresher(
      controller: _refreshController,
      //默认为true
      enablePullDown: true,
      //默认为false,不设置不支持上拉加载
      enablePullUp: true,
      //这个页面是瀑布流效果,可以更换
      header: const MaterialClassicHeader(),
      // footer: const ClassicFooter(),
      footer: CustomFooter(
        builder: (BuildContext context,LoadStatus? mode){
          Widget body;
          switch(mode) {
            case LoadStatus.idle:
              body = const Text('上拉加载');
              break;
            case LoadStatus.loading:
              body = const CupertinoActivityIndicator();
              break;
            case LoadStatus.failed:
              body = const Text("加载失败, 点击重试!");
              break;
            case LoadStatus.canLoading:
              body = const Text("松手,加载更多!");
              break;
            default:
              body = const Text("没有更多数据了!");
              break;
          }
          return SizedBox(
            height: 55.0,
            child: Center(child:body),
          );
        },
      ),
      onRefresh: onRefresh,
      onLoading: loadMore,
      child: _bodyListViewWidget(),
    );
  }

  /// 列表布局
  Widget _bodyListViewWidget() {
    return CustomScrollView(
      slivers: [
        const BannerViewWidget(),
        HomeListView(),
      ],
    );
  }

  void initAsyncState() {
    // 发送请求
    BannerEvent.send(context);
    HomeListEvent.send(context, _refreshController);
  }

  /// 下拉刷新
  void onRefresh() {
    page = 1;
    // 发送请求
    BannerEvent.send(context);
    HomeListEvent.send(context, _refreshController);
  }

  // 上拉加载
  void loadMore() {
    page++;
    HomeListEvent.send(context, _refreshController, page: page - 1);
  }
}

总结

关于bloc的使用后续会继续有延伸,本篇主要记录两点,一个是关于bloc多状态拆分细化,一个是下拉刷新控件搭配bloc的使用,其中也包含对 CustomScrollView的使用,注意:pull_to_refresh中SmartRefresher的child必须是Listview或CustomScrollView等,这个坑由于一开始使用bloc就没分开使用所以并没踩到但需要注意,同时本篇文章主要是将上一篇文章中对bloc的使用做了纠正,可以将多状态放在一个state中使用,但是没办法局部刷新子布局,控制粒度低,另外结合了官方示例,改变上一篇文章中书写state比较麻烦的情况,最开始的思路是将多个状态放到一个state文件中实现,通过sealed class来判断具体是哪个数据,但结果是最终只返回了列表数据,其实想想也能理解,毕竟只发起了一次事件不可能有两次结果,即便我响应了两次第一次结果也会被后面的覆盖,解决方案是放两个参数分别接收,但同时我考虑到这种方式不优雅,也不是一定需要两个状态同时判断,又将状态拆分成独立状态搭配子布局使用,通过这种方式也完美解决不够优雅的问题,也许最开始拆分思路是对的,但是方向错了,其实只需要将RefreshController 传递过来内部处理即可实现刷新控件无法正常收起的情况,如有更好的思路或不对的地方欢迎交流与指正。

你可能感兴趣的:(flutter,bloc,pull_to_refresh)