前言
当你了解了Provider,并打算用到你的项目中,这篇文章可以帮你快速进入实战开发。
为何要有代码框架
前面的笔记介绍了Provider的简易使用Demo和源码。
从Demo到项目落地,有个过程。
数据与UI的交互本身,说简单也简单,说复杂也复杂。不同的人写出来,必然是不尽相同的。光状态管理就多种方式。
- 数据处理
- 事件分发
- 状态管理-组件内/跨组件/跨页面
我们得有个代码框架,减少学习成本,减少后期维护成本。接下来就是基于Provider,同时结合项目体会,写了一个代码框架。
业务场景
假定我们有一个社区的应用。我们有两个页面
- 帖子列表页面,展示帖子部分正文/点赞与否。点击帖子,进入到帖子详情页面,但不可点赞/取消点赞。
- 帖子详情页面,展示帖子正文内容/点赞与否。可点赞/取消点赞,同时当回到列表页,需要显示最新的点赞状态。
效果图
需求分析
- 1.进入列表页,请求server列表,展示。
- 2.点击列表某一项,传id,跳转详情页。
- 3.进入详情页,根据id请求server详情,展示。
- 4.详情页点赞,ask server,详情页更新点赞状态,全局通知点赞状态变更。
- 5.列表页接收通知,变更对应帖子的点赞状态。
框架思路
数据处理与UI分离。 这是移动开发遵守的框架规则,比如MVP,MVVM等等。 这是一个大致的结构描述。注意点:
- ChangeNotifierProvider把我们实现了InteritedElement,Element部分。
- MyWidget是页面组件,需要我们自己画
- MyModel是数据处理地方。
- Notify时,并没有把Model(subject) 传递出去,而是要从Inherited中获取。
技术要点
ChangNotifier
:实现Listenable接口,观察者模式实现。ChangNotifierProvider
:ChangNotifier作为其参数,ChangNotifier.notifyListeners触发其刷新逻辑ItemRefresher
:列表Item单个刷新功能,参考 Provider中的SelectorMultiProvider
:Provider库中,方便Widget使用多个Provider,不用的话,就嵌套多层ProviderEventBus
:event_bus:^1.1.1 pub上事件总线的某个实现SmartRefresher
:上拉下拉刷新加载组件
框架实现
帖子实体类
首先我们要有个实体类定义帖子。
class PostBean {
int id;//唯一标识
String content; // 正文
bool isLike;// 点赞与否
Mock Server
我们从客户端的视角来看,需要一个Server。这里我们mock下。Client与Server用JSON交互。Server应该具备以下接口:获取列表
,获取详情
,请求点赞
。
class PostServer{
///获取列表
//返回JSON列表,可转换为PostBean列表
Future<List<Map<String, dynamic>>> loadPosts() async
///获取详情
//返回JSON,可转换为PostBean对象
Future<Map<String, dynamic>> getPostDetail(int id) async
///请求点赞
//返回是否操作成功 {"success": true}
Future<Map<String, dynamic>> like(int id, bool toLike) async
}
事件总线
EventBus eventBus = EventBus();
class BaseEvent {
void fire() {
eventBus.fire(this);
}
}
class PostLikeEvent extends BaseEvent with ChangeNotifier{
int id;
bool isLike;
PostLikeEvent(this.id, this.isLike);
}
页面构建
我们有两个页面,列表页和详情页。 页面分成两个组件:Widget和Model。
Widget
UI部分Model
可以理解成MVVM的ViewModel或者MVP的Presenter。负责数据的获取和处理。
列表页
PostListModel
class PostListModel with ChangeNotifier {
var posts = new List<PostBean>();
///smartRefresher的刷新控制器
RefreshController refreshController = RefreshController();
///解除事件监听方法
VoidCallback _eventDispose;
/// 单个刷新的ChangeNotifier
PostListItemListenable itemListenable;
PostListModel() {
itemListenable = new PostListItemListenable();
}
///订阅PostLikeEvent
void subscribePostLike() {
StreamSubscription subscription =
eventBus.on<PostLikeEvent>().listen((event) {
///拿到event,更新下当前页面对应post的isLike状态
posts?.firstWhere((post) => post.id == event.id, orElse: () => null)
?.isLike = event.isLike;
});
_eventDispose = () => subscription.cancel();
}
///加载数据
void loadData({VoidCallback callback}) {
PostServer.instance().loadPosts().then((jsonList) {
posts = jsonList.map((json) => PostBean.map(json)).toList();
notifyListeners();
callback.call();
}).catchError((e) => print(e));
}
///下拉刷新,数据获取到后,通知smartRefresher
void refresh() {
loadData(callback: () => refreshController.refreshCompleted());
}
///ChangeNotifier的 解除监听方法。
@override
void dispose() {
super.dispose();
_eventDispose?.call();
}
}
PostListItemListenable
class PostListItemListenable with ChangeNotifier {
int id;
}
PostListWidget
class PostListWidget extends StatefulWidget {
.....省略部分代码
}
class _PostListWidgetState extends State<PostListWidget> {
PostListModel _listModel;
@override
void initState() {
super.initState();
///初始化构建Model,同时加载数据
_listModel = PostListModel()..loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
.....
///多Provider使用,提前设置好
body: MultiProvider(
providers: [
ChangeNotifierProvider.value(value: _listModel),
ChangeNotifierProvider.value(value: _listModel.itemListenable),
],
child: Consumer<PostListModel>(
builder: (context, model, child) {
Widget child = ListView.separated(
itemBuilder: (itemContext, index) {
return _buildListItem(itemContext, model.posts[index]);
},....);
///设置 SmartRefresher: refreshController,onRefresh。
///onRefresh回调。widget不处理数据相关的回调,而是交给model处理
return SmartRefresher(
controller: model.refreshController,
enablePullDown: true,
enablePullUp: true,
onRefresh: () => model.refresh(),
child: child,
);
},
),
),
);
}
Widget _buildListItem(BuildContext context, PostBean post) {
///ItemRefresher 自定义的列表item刷新利器
return ItemRefresher<PostListItemListenable, PostBean>(
value: post,
shouldRebuild: (itemListenable, value) =>
(itemListenable.id != null && itemListenable.id == value.id),
builder: (context, value, child) {
return PostItemWidget(
post: value,
click: _skipPostDetail,
);
},
);
}
_skipPostDetail(BuildContext context, PostBean post) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => PostDetailWidget(id: post.id),
));
}
PostItemWidget
class PostItemWidget extends StatelessWidget {
final PostBean post;
///点击回调处理
final void Function(BuildContext context, PostBean post) click;
const PostItemWidget({Key key, this.post, this.click}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => click?.call(context, post),
child: Container(
height: 80,
child: Row(
children: <Widget>[
Expanded(
child: Text(
"${post?.content}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Container(
width: 50,
child: Icon(
Icons.favorite,
color: (post?.isLike ?? false) ? Colors.red : Colors.grey,
),
),
],
),
));
}
}
详情页及点赞
详情页结构与列表页相似。我们单独点出来 点赞的处理部分。
PostDetailModel
class PostDetailModel with ChangeNotifier {
PostBean post;
initPost(int id) {
PostServer.instance().getPostDetail(id).then((json) {
post = PostBean.map(json);
notifyListeners();
}).catchError((e) => print(e));
}
likePost(bool toLike) {
PostServer.instance().like(post.id, toLike).then((result) {
if (result["success"]) {
post.isLike = toLike;
///EventBus 全局事件通知
PostLikeEvent(post.id, toLike).fire();
}
///通知PostDetailWidget刷新
notifyListeners();
}).catchError((e) => print(e));
}
}
这篇文章中涉及到了EventBus,MultiProvider,Selector
部分内容在下面: