前言
當你瞭解了Provider,並打算用到你的專案中,這篇文章可以幫你快速進入實戰開發。
Demo倉庫地址 入口:main_provider.dart -- 程式碼框架
為何要有程式碼框架
前面的筆記介紹了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等等,我們後續會分析分析。