Flutter状态管理Provider(三)开发应用

4,721 阅读4分钟

前言

当你了解了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中的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

部分内容在下面:

Selector使用 EventBus