前言
思考:这篇文章主要是记录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 传递过来内部处理即可实现刷新控件无法正常收起的情况,如有更好的思路或不对的地方欢迎交流与指正。