这篇是使用 Bloc 来实现业务逻辑与UI分离。主要就是慕课网课程列表的网络请求并且展示。
首先定义一个基础事件的类,如下:
abstract class LessonEvent {}
然后我定义了3种事件:
- 发起网络请求事件:
class FetchDataEvent extends LessonEvent {}
- 网络请求成功事件:
class FetchDataSuccessEvent extends LessonEvent {}
- 网络请求失败的事件:
class FetchDataFailedEvent extends LessonEvent {}
然后定义一个枚举类型,来表示这3种事件,这样就可以在 BlockBuild 中根据枚举类型来展示不同的 UI 了。
enum LoadStatus { loading, success, failed }
课程的数据结构如下:
import 'package:json_annotation/json_annotation.dart';
part 'LessonBean.g.dart';
@JsonSerializable()
class LessonBean {
int status;
String msg;
List<LessonDetail> data;
LessonBean({required this.status, required this.msg, required this.data});
factory LessonBean.fromJson(Map<String, dynamic> srcJson) =>
_$LessonBeanFromJson(srcJson);
Map<String, dynamic> toJson() => _$LessonBeanToJson(this);
@override
bool operator ==(Object other) {
if (identical(other, this)) return true;
if (other is LessonBean) {
return other.runtimeType == this.runtimeType &&
other.status == this.status &&
other.msg == this.msg &&
other.data == this.data;
} else {
return false;
}
}
@override
int get hashCode {
int result = 17;
result = 37 * result + status.hashCode;
result = 37 * result + msg.hashCode;
result = 37 * result + data.hashCode;
return result;
}
}
@JsonSerializable()
class LessonDetail {
final int id;
final String name;
final String picSmall;
final String picBig;
final String description;
LessonDetail(
{required this.id,
required this.name,
required this.picSmall,
required this.picBig,
required this.description});
factory LessonDetail.fromJson(Map<String, dynamic> srcJson) =>
_$LessonDetailFromJson(srcJson);
Map<String, dynamic> toJson() => _$LessonDetailToJson(this);
@override
bool operator ==(Object other) {
if (identical(other, this)) {
return true;
} else if (other is LessonDetail) {
return other.runtimeType == this.runtimeType &&
other.id == this.id &&
other.name == this.name &&
other.picSmall == this.picSmall &&
other.picBig == this.picBig &&
other.description == this.description;
} else {
return false;
}
}
@override
int get hashCode {
int result = 17;
result = 37 * result + id.hashCode;
result = 37 * result + name.hashCode;
result = 37 * result + picSmall.hashCode;
result = 37 * result + picBig.hashCode;
result = 37 * result + description.hashCode;
return result;
}
}
重写了其中的 hashcode 和重载了运算符 ==
。
然后再定义一个课程的包装类,包含了 LessonBean
和 枚举类型 LoadStatus
,如下:
class LessonWrap {
LessonBean? lessonBean;
LoadStatus status;
LessonWrap({required this.lessonBean, required this.status});
}
这个包装类型 LessonWrap
将作为自定义 Bloc 的输出,然后 BlockBuilder中就会观察到数据的改变。
LessonBloc 的代码如下:
class LessonBloc extends Bloc<LessonEvent, LessonWrap> {
LessonBean? _lessonBean;
LessonBloc(LessonWrap initial) : super(initial) {
on<FetchDataEvent>((event, emit) {
_requestData();
});
on<FetchDataSuccessEvent>((event, emit) {
emit(LessonWrap(lessonBean: _lessonBean, status: LoadStatus.success));
});
on<FetchDataFailedEvent>((event, emit) {
emit(LessonWrap(lessonBean: null, status: LoadStatus.failed));
});
/// 初始化 bloc 的时候就去请求数据
add(FetchDataEvent());
}
void _requestData() async {
_lessonBean = await MukeService().getPersonalLesson(7);
/// 接口请求太快了,看不到loading圈,所以加个延时
Timer(Duration(seconds: 2), () {
if (_lessonBean == null) {
add(FetchDataFailedEvent());
} else {
add(FetchDataSuccessEvent());
}
});
}
}
页面展示的逻辑如下:
class BlocInstanceWidget extends StatelessWidget {
Widget getRow(LessonDetail detail) {
return GestureDetector(
child: Container(
color: Colors.blueGrey,
width: double.infinity,
padding: EdgeInsets.only(bottom: 10, left: 10, top: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("Row >>> ${detail.name}"),
SizedBox(
height: 5,
),
Container(
child: ClipRRect(
// 圆角图片
borderRadius: BorderRadius.circular(8),
child: Image(
image: NetworkImage(detail.picSmall),
width: 60,
height: 60,
fit: BoxFit.fitHeight,
),
),
),
SizedBox(
height: 6,
),
Text(
detail.description,
style: const TextStyle(
color: Colors.brown,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
LessonBloc lessonBloc = BlocProvider.of<LessonBloc>(context);
return Scaffold(
appBar: AppBar(
title: Text('Bloc Instance'),
),
body: Container(
child: Column(
children: [
BlocBuilder<LessonBloc, LessonWrap>(
builder: (context, wrap) {
if (wrap.status == LoadStatus.loading) {
return Container(
margin: EdgeInsets.only(top: 300),
// color: Color(0xffFFFFFF),
alignment: Alignment.center,
child: CircularProgressIndicator(
color: const Color(0xff2A9DFF).withOpacity(0.5),
));
} else if (wrap.status == LoadStatus.success) {
/// 因为 在 Column 中嵌套 ListView ,所以需要加上 Expanded ,
/// 否则报 Vertical viewport was given unbounded height
return Expanded(
child: ListView.separated(
shrinkWrap: true,
itemCount: wrap.lessonBean!.data.length,
itemBuilder: (BuildContext context, int position) {
return getRow(wrap.lessonBean!.data[position]);
},
separatorBuilder: (context, index) {
return Divider(
height: 1.0, indent: 10, color: Colors.black);
},
));
} else {
return Text("请求数据出错!!!!");
}
},
buildWhen: (previous, next) {
/// 过滤条件,只有2次数据不一致时才刷新
return previous.lessonBean != next.lessonBean;
},
),
ElevatedButton(
onPressed: () {
/// 由于我加了过滤条件,即上面的 buildWhen,
/// 因为2次刷新的数据是一样的(我重写了 LessonBean 的 hashcode 和 '=='),所以不会刷新界面
lessonBloc.add(FetchDataEvent());
},
child: Text('刷新数据')),
],
),
),
);
}
}
入口代码如下:
void main() {
runApp(MaterialApp(
home: BlocProvider<LessonBloc>(
create: (context) => LessonBloc(
LessonWrap(lessonBean: null, status: LoadStatus.loading)),
child: BlocInstanceWidget())));
}