Flutter狀態管理Provider(三)基於Provider的程式碼框架

mwq30123發表於2020-04-17

前言

當你瞭解了Provider,並打算用到你的專案中,這篇文章可以幫你快速進入實戰開發。

Demo倉庫地址 入口:main_provider.dart -- 程式碼框架

為何要有程式碼框架

前面的筆記介紹了Provider的簡易使用Demo和原始碼。

從Demo到專案落地,有個過程。

資料與UI的互動本身,說簡單也簡單,說複雜也複雜。不同的人寫出來,必然是不盡相同的。光狀態管理就多種方式。

  • 資料處理
  • 事件分發
  • 狀態管理-元件內/跨元件/跨頁面

我們得有個程式碼框架,減少學習成本,減少後期維護成本。接下來就是基於Provider,同時結合專案體會,寫了一個程式碼框架。

業務場景

假定我們有一個社群的應用。我們有兩個頁面

  • 帖子列表頁面,展示帖子部分正文/點贊與否。點選帖子,進入到帖子詳情頁面,但不可點贊/取消點贊。
  • 帖子詳情頁面,展示帖子正文內容/點贊與否。可點贊/取消點贊,同時當回到列表頁,需要顯示最新的點贊狀態。

效果圖

Flutter狀態管理Provider(三)基於Provider的程式碼框架

需求分析

  • 1.進入列表頁,請求server列表,展示。
  • 2.點選列表某一項,傳id,跳轉詳情頁。
  • 3.進入詳情頁,根據id請求server詳情,展示。
  • 4.詳情頁點贊,ask server,詳情頁更新點贊狀態,全域性通知點贊狀態變更。
  • 5.列表頁接收通知,變更對應帖子的點贊狀態。

框架思路

資料處理與UI分離。 這是移動開發遵守的框架規則,比如MVP,MVVM等等。

Flutter狀態管理Provider(三)基於Provider的程式碼框架
這是一個大致的結構描述。注意點:

  • ChangeNotifierProvider把我們實現了InteritedElement,Element部分。
  • MyWidget是頁面元件,需要我們自己畫
  • MyModel是資料處理地方。
  • Notify時,並沒有把Model(subject) 傳遞出去,而是要從Inherited中獲取。

技術要點

  • ChangNotifier:實現Listenable介面,觀察者模式實現。
  • ChangNotifierProvider:ChangNotifier作為其引數,ChangNotifier.notifyListeners觸發其重新整理邏輯
  • ItemRefresher:列表Item單個重新整理功能,參考 Provider中的Selector
  • MultiProvider:Provider庫中,方便Widget使用多個Provider,不用的話,就巢狀多層Provider
  • EventBus: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等等,我們後續會分析分析。

相關文章