zoukankan      html  css  js  c++  java
  • mvp在flutter中的应用

    mvp模式的优点
    mvp模式将视图、业务逻辑、数据模型隔离,使用mvp模式,能使复杂的业务逻辑变得更加清晰,使代码更具有灵活性和扩展性,正是这些优点,使mvp模式广泛应用于原生开发中。

    flutter使用mvp之前
    以前原生开发页面,只需要花费少量的时间,就可以通过原生提供的可视化拖拽功能,迅速的完成一个简单的页面布局效果和配置,而逻辑代码只需要引用布局文件即可完成交互。然而flutter开发中目前还没有提供可视化的拖拽功能,实现页面布局和控件需要一行行码代码,因此在页面布局、元素上将会花费大量的编码时间,这对于原生开发的工程师的我来说,感觉十分不习惯。既然现在还没有提供可视化UI编辑功能,那我们也只能按照标准一行行编写UI了。flutter核心要素就是widget,所有页面元素都是widget,下面来看看使用mvp之前的代码:

    import 'dart:convert';
     
    import 'package:badge/badge.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_biobank/entity/IntResult.dart';
    import 'package:flutter_biobank/entity/SampleResult.dart';
    import 'package:flutter_biobank/entity/TextResult.dart';
    import 'package:flutter_biobank/page/work/SampleCartsPage.dart';
    import 'package:flutter_biobank/res/colors.dart';
    import 'package:flutter_biobank/res/images.dart';
    import 'package:flutter_biobank/res/urls.dart';
    import 'package:flutter_biobank/util/DialogUtil.dart';
    import 'package:flutter_biobank/util/HttpUtil.dart';
    import 'package:flutter_biobank/util/NavigatorUtil.dart';
    import 'package:flutter_biobank/widget/PageLoadView.dart';
    import 'package:flutter_biobank/widget/SmartRefresh.dart';
    import 'package:fluttertoast/fluttertoast.dart';
    import "package:pull_to_refresh/pull_to_refresh.dart";
     
    ///样本申领
    class SampleClaimPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return new _SampleClaimPageState();
      }
    }
     
    class _SampleClaimPageState extends State<SampleClaimPage> {
      int pageIndex = 1; //当前页码
      RefreshController _controller;
      List<Sample> samples = new List(); //列表中的样本集合
      List<Sample> checkedSamples = new List(); //选中的样本集合
      int loadStatus; //当前页面加载状态
      int samplesCount = 0; //申领车中样本数量
     
      @override
      void initState() {
        super.initState();
        _controller = new RefreshController();
        getSamples();
        getSamplesCount();
      }
     
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
          backgroundColor: page_background,
          appBar: new AppBar(
            title: new Text("样本申领"),
            actions: <Widget>[
              Container(
                alignment: Alignment.center,
                margin: EdgeInsets.only(right: 8),
                child: new Badge.left(
                    child: IconButton(
                      icon: ImageIcon(AssetImage(Images.IMG_ICON_CARTS)),
                      onPressed: () {
                        NavigatorUtil.startIntent(context, new SampleCartsPage());
                      },
                    ),
                    positionTop: 0,
                    borderSize: 0,
                    positionRight: 0,
                    value: " $samplesCount "),
              )
            ],
          ),
          body: new PageLoadView(
            status: loadStatus,
            child: _buildContent(),
            offstage: samples.length > 0,
            onTap: () {
              setState(() {
                loadStatus = PageLoadStatus.loading;
                getSamples();
              });
            },
          ),
        );
      }
     
      ///构建内容显示布局
      Widget _buildContent() {
        return new Column(
          children: <Widget>[
            _buildRefresh(),
            _buildBottom(),
          ],
        );
      }
     
      ///构建底部控件
      Widget _buildBottom() {
        return Container(
          color: Colors.white,
          child: Column(
            children: <Widget>[
              new Divider(height: 0.5, color: devider_black),
              new Row(
                children: <Widget>[
                  Expanded(child: Container(child: new Text("选中样本:${checkedSamples.length}"), padding: EdgeInsets.symmetric(horizontal: 16))),
                  GestureDetector(
                    child: new Container(
                        alignment: Alignment.center,
                        color: Colors.red,
                         120,
                        height: 60,
                        child: new Text("加入申领", style: new TextStyle(color: Colors.white, fontSize: 16))),
                    onTap: () {
                      if (checkedSamples.length <= 0) {
                        Fluttertoast.showToast(msg: "请选择样本");
                      } else {
                        doJoinCarts();
                      }
                    },
                  ),
                ],
              ),
            ],
          ),
        );
      }
     
      ///构建刷新和加载控件
      Widget _buildRefresh() {
        return new SmartRefresh(
          controller: _controller,
          child: _buildListView(),
          onRefresh: () {
            //下拉刷新
            pageIndex = 1;
            return getSamples();
          },
          onLoadMore: (bool) {
            //上拉加载更多
            getSamples();
          },
        );
      }
     
      ///构建ListView
      Widget _buildListView() {
        return new ListView.builder(
          physics: new AlwaysScrollableScrollPhysics(),
          itemBuilder: _buildListViewItem,
          itemCount: samples.length,
        );
      }
     
      ///构建listItem
      Widget _buildListViewItem(BuildContext context, int index) {
        return Card(
          child: Container(
            padding: EdgeInsets.symmetric(vertical: 8, horizontal: 2),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                new Checkbox(
                    value: samples[index].isSelected,
                    onChanged: (bool) {
                      setState(() {
                        samples[index].isSelected = bool;
                        bool ? checkedSamples.add(samples[index]) : checkedSamples.remove(samples[index]);
                      });
                    }),
                new Expanded(
                    child: new Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    new Text("${samples[index].SerialNumber}", style: new TextStyle(fontSize: 18)),
                    new Text(
                      "样本名称:${samples[index].Name}",
                      style: new TextStyle(color: Colors.black45),
                      softWrap: false,
                      overflow: TextOverflow.fade,
                    ),
                    new Text("可用量:${samples[index].AvailableVolume}", style: new TextStyle(color: Colors.black45)),
                    new Text("位置:${samples[index].Location}", style: new TextStyle(color: Colors.black45)),
                  ],
                )),
              ],
            ),
          ),
        );
      }
     
      ///加载数据
      Future getSamples() async {
        await HttpUtil.getInstance().post(Urls.URL_GET_SAMPLES, data: {
          "PageIndex": pageIndex,
          "PageSize": 20,
          "State": 1,
          "Sort": "asc",
          "BeginTime": "",
          "BoxCode": "",
          "EndTime": "",
          "Location": "",
          "Name": "",
          "ProjectID": null,
          "erialNumber": "",
          "StudyID": null,
        }, callBack: (success, data) {
          _controller.sendBack(false, RefreshStatus.idle);
          if (success) {
            SampleResult result = SampleResult.fromJson(json.decode(data)); //解析json
            if (result.code == 200) {
              if (pageIndex == 1) {
                samples.clear(); //下拉刷新时,先清除原来的数据
                checkedSamples.clear(); //下拉刷新时,选中的数据也清空
              }
              samples.addAll(result.rows);
              pageIndex += 1; //数据加载成功后,page+1
              samples.length >= result.total ? _controller.sendBack(false, RefreshStatus.noMore) : null;
            } else {
              Fluttertoast.showToast(msg: result.message);
              loadStatus = PageLoadStatus.failed;
            }
          } else {
            loadStatus = PageLoadStatus.failed;
          }
          setState(() {});
        });
      }
     
      ///加入申领车
      Future doJoinCarts() async {
        DialogUtil.showLoading(context); //显示加载对话框
        List<int> ids = new List();
        for (Sample sample in checkedSamples) {
          ids.add(sample.ID);
        }
        await HttpUtil.getInstance().post(Urls.URL_CARTS_ADD, data: json.encode(ids), callBack: (success, data) {
          Navigator.pop(context);
          if (success) {
            TextResult result = TextResult.fromJson(json.decode(data));
            DialogUtil.showTips(context, text: result.message);
            pageIndex = 1;
            getSamples();
            getSamplesCount();
          } else {
            Fluttertoast.showToast(msg: data);
          }
        });
      }
     
      ///查询申领车中样本数量
      Future getSamplesCount() async {
        await HttpUtil.getInstance().get(Urls.URL_CARTS_SAMPLESCOUNT, callBack: (success, data) {
          if (success) {
            IntResult result = IntResult.fromJson(json.decode(data));
            if (result.code == 200) {
              setState(() {
                samplesCount = result.response;
              });
            }
          }
        });
      }
    }

    这只是一个简单的页面,调用列表查询接口,用ListView显示列表数据,调用数量查询接口,查询购物车中数量,并显示在appBar的action中。业务逻辑和页面代码混合在一起,导致代码量大,类看起来很臃肿,如果业务逻辑更加复杂的情况下,代码阅读、代码审查及功能维护都不是件容易的事。

    flutter使用mvp之后
    下面我们使用mvp模式对上面的代码进行改造,首先我们先将view的改变行为抽象出来,建立viewMode

    abstract class IClaimPageView {
      void querySamplesSuccess(SampleResult result);
     
      void querySamplesFailed();
     
      void queryCartsSampleCountSuccess(int count);
     
      void doJoinCartsSuccess(String message);
     
      void doJoinCartsFailed(String message);
    }

    该类定义的方法分别表示列表查询成功或失败了,查询数量成功了,加入购物车成功或失败了,页面分别要做的各种事情,具体页面要做什么变化,就交给View去实现,也就是页面View,实现viewModel。

    class _SampleClaimPageState extends State<SampleClaimPage> implements IClaimPageView {
      @override
      void querySamplesSuccess(SampleResult result) {
          //TODO
      }
     
      @override
      void querySamplesFailed() {
          //TODO
      }
     
      @override
      void queryCartsSampleCountSuccess(int count) {
          //TODO
      }
     
      @override
      void doJoinCartsFailed(String message) {
          //TODO
      }
     
      @override
      void doJoinCartsSuccess(String message) {
          //TODO
      }
    }

    接下来,我们需要将业务逻辑代码分离出去,建立Presenter类,传入viewModel的引用,并定义方法实现业务逻辑。

    ///样本申领presenter
    class ClaimPresenter extends BasePresenter {
      ClaimModel _model;
      IClaimPageView _view;
     
      ClaimPresenter(this._view) {
        _model = new ClaimModel();
      }
     
      ///分页查询库存中的样本
      Future querySamples(int pageIndex) async {
        await _model.querySamples({
          "PageIndex": pageIndex,
          "PageSize": 20,
          "State": 1,
          "Sort": "asc",
          "BeginTime": "",
          "BoxCode": "",
          "EndTime": "",
          "Location": "",
          "Name": "",
          "ProjectID": null,
          "erialNumber": "",
          "StudyID": null,
        }, (bool, result) {
          if (_view == null) {
            return;
          }
          if (bool) {
            _view.querySamplesSuccess(result);
          } else {
            _view.querySamplesFailed();
          }
        });
      }
     
      ///查询申领车中样本数量
      Future queryCartsSampleCount() async {
        await _model.queryCartsSampleCount((bool, int) {
          if (_view == null) {
            return;
          }
          if (bool) {
            _view.queryCartsSampleCountSuccess(int);
          }
        });
      }
     
      ///加入申领车
      Future doJoinCarts(BuildContext context, List<Sample> samples) async {
        DialogUtil.showLoading(context);
        List<int> ids = new List();
        for (Sample sample in samples) {
          ids.add(sample.ID);
        }
        await _model.doJoinCarts(json.encode(ids), (bool, message) {
          Navigator.pop(context);
          if (_view == null) {
            return;
          }
          if (bool) {
            _view.doJoinCartsSuccess(message);
          } else {
            _view.doJoinCartsFailed(message);
          }
        });
      }
     
      @override
      void dispose() {
        _view = null;
      }
    }

    这里的ClaimModel 实际上就是数据请求代码,原本数据请求也是可以写在presenter类中的,但是为了使代码更具灵活性和解耦性,我们这里将数据请求层也抽取出去,这样我其它页面也需要查询购物车中样本数量时,只需要几句简单的代码即可实现。

    class ClaimModel {
      ///查询样本列表
      Future querySamples(data, Function(bool, Object) callBack) async {
        await HttpUtil.getInstance().post(Urls.URL_GET_SAMPLES, data: data, callBack: (success, data) {
          if (success) {
            SampleResult result = SampleResult.fromJson(json.decode(data)); //解析json
            if (result.code == 200) {
              callBack(true, result);
            } else {
              callBack(false, result.message);
            }
          } else {
            callBack(false, data);
          }
        });
      }
     
      ///查询申领车中的样本数量
      Future queryCartsSampleCount(Function(bool, int) callBack) async {
        await HttpUtil.getInstance().get(Urls.URL_CARTS_SAMPLESCOUNT, callBack: (success, data) {
          if (success) {
            IntResult result = IntResult.fromJson(json.decode(data));
            if (result.code == 200) {
              callBack(true, result.response);
            }
          }
        });
      }
     
      ///加入申领车
      Future doJoinCarts(data, Function(bool, String) callBack) async {
        await HttpUtil.getInstance().post(Urls.URL_CARTS_ADD, data: data, callBack: (success, data) {
          if (success) {
            TextResult result = TextResult.fromJson(json.decode(data));
            callBack(true, result.message);
          } else {
            callBack(true, data);
          }
        });
      }
    }

    最后就是View的完整代码了

    import 'package:badge/badge.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_biobank/entity/SampleResult.dart';
    import 'package:flutter_biobank/page/work/SampleCartsPage.dart';
    import 'package:flutter_biobank/page/work/claim/ClaimPresenter.dart';
    import 'package:flutter_biobank/page/work/claim/IClaimPageView.dart';
    import 'package:flutter_biobank/res/colors.dart';
    import 'package:flutter_biobank/res/images.dart';
    import 'package:flutter_biobank/util/DialogUtil.dart';
    import 'package:flutter_biobank/util/Logger.dart';
    import 'package:flutter_biobank/util/NavigatorUtil.dart';
    import 'package:flutter_biobank/widget/PageLoadView.dart';
    import 'package:flutter_biobank/widget/SmartRefresh.dart';
    import 'package:fluttertoast/fluttertoast.dart';
    import "package:pull_to_refresh/pull_to_refresh.dart";
     
    ///样本申领
    class SampleClaimPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return new _SampleClaimPageState();
      }
    }
     
    class _SampleClaimPageState extends State<SampleClaimPage> implements IClaimPageView {
      static final String TAG = "SampleClaimPageState";
      int pageIndex = 1; //当前页码
      RefreshController _controller; //控制器,控制加载更多的显示状态
      List<Sample> samples = new List(); //列表中的样本集合
      List<Sample> checkedSamples = new List(); //选中的样本集合
      int loadStatus; //当前页面加载状态
      int samplesCount = 0; //申领车中样本数量
      ClaimPresenter _presenter;
     
      @override
      void initState() {
        super.initState();
        Logger.log(TAG, "initState");
        _controller = new RefreshController();
        _presenter = new ClaimPresenter(this);
        _presenter.querySamples(pageIndex);
        _presenter.queryCartsSampleCount();
      }
     
      @override
      void dispose() {
        super.dispose();
        if (_presenter != null) {
          _presenter.dispose();
          _presenter = null;
        }
      }
     
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
          backgroundColor: page_background,
          appBar: new AppBar(
            title: new Text("样本申领"),
            actions: <Widget>[
              Container(
                alignment: Alignment.center,
                margin: EdgeInsets.only(right: 8),
                child: _buildBadge(context),
              )
            ],
          ),
          body: _buildBody(),
        );
      }
     
      ///构建购物车按钮
      Widget _buildBadge(BuildContext context) {
        if (samplesCount > 0) {
          return new Badge.left(
              child: IconButton(
                icon: ImageIcon(AssetImage(Images.IMG_ICON_CARTS)),
                onPressed: () {
                  NavigatorUtil.startIntent(context, new SampleCartsPage());
                },
              ),
              positionTop: 0,
              borderSize: 0,
              positionRight: 0,
              value: " $samplesCount ");
        } else {
          return new IconButton(
            icon: ImageIcon(AssetImage(Images.IMG_ICON_CARTS)),
            onPressed: () {
              NavigatorUtil.startIntent(context, new SampleCartsPage());
            },
          );
        }
      }
     
      ///构建body
      Widget _buildBody() {
        return new PageLoadView(
          status: loadStatus,
          child: new Column(
            children: <Widget>[
              _buildRefresh(),
              _buildBottom(),
            ],
          ),
          offstage: samples.length > 0,
          onTap: () {
            setState(() {
              loadStatus = PageLoadStatus.loading;
              _presenter.querySamples(pageIndex);
            });
          },
        );
      }
     
      ///构建底部控件
      Widget _buildBottom() {
        return Container(
          color: Colors.white,
          child: Column(
            children: <Widget>[
              new Divider(height: 0.5, color: devider_black),
              new Row(
                children: <Widget>[
                  Expanded(child: Container(child: new Text("选中样本:${checkedSamples.length}"), padding: EdgeInsets.symmetric(horizontal: 16))),
                  GestureDetector(
                    child: new Container(
                        alignment: Alignment.center,
                        color: Colors.red,
                         120,
                        height: 60,
                        child: new Text("加入申领", style: new TextStyle(color: Colors.white, fontSize: 16))),
                    onTap: () {
                      if (checkedSamples.length <= 0) {
                        Fluttertoast.showToast(msg: "请选择样本");
                      } else {
                        _presenter.doJoinCarts(context, checkedSamples);
                      }
                    },
                  ),
                ],
              ),
            ],
          ),
        );
      }
     
      ///构建刷新和加载控件
      Widget _buildRefresh() {
        return new SmartRefresh(
          controller: _controller,
          child: _buildListView(),
          onRefresh: () {
            return _presenter.querySamples(pageIndex = 1); //下拉刷新
          },
          onLoadMore: (bool) {
            //上拉加载更多
            _presenter.querySamples(pageIndex);
          },
        );
      }
     
      ///构建ListView
      Widget _buildListView() {
        return new ListView.builder(
          physics: new AlwaysScrollableScrollPhysics(),
          itemBuilder: _buildListViewItem,
          itemCount: samples.length,
        );
      }
     
      ///构建listItem
      Widget _buildListViewItem(BuildContext context, int index) {
        return Card(
          child: Container(
            padding: EdgeInsets.symmetric(vertical: 8, horizontal: 2),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                new Checkbox(
                    value: samples[index].isSelected,
                    onChanged: (bool) {
                      setState(() {
                        samples[index].isSelected = bool;
                        bool ? checkedSamples.add(samples[index]) : checkedSamples.remove(samples[index]);
                      });
                    }),
                new Expanded(
                    child: new Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    new Text("${samples[index].SerialNumber}", style: new TextStyle(fontSize: 18)),
                    new Text(
                      "样本名称:${samples[index].Name}",
                      style: new TextStyle(color: Colors.black45),
                      softWrap: false,
                      overflow: TextOverflow.fade,
                    ),
                    new Text("可用量:${samples[index].AvailableVolume}", style: new TextStyle(color: Colors.black45)),
                    new Text("位置:${samples[index].Location}", style: new TextStyle(color: Colors.black45)),
                  ],
                )),
              ],
            ),
          ),
        );
      }
     
      @override
      void querySamplesSuccess(SampleResult result) {
        //下拉刷新需要先清空列表数据
        if (pageIndex == 1) {
          samples.clear();
          checkedSamples.clear();
        }
        samples.addAll(result.rows);
        //判断是不是最后一页
        pageIndex * 20 >= result.total ? _controller.sendBack(false, RefreshStatus.noMore) : _controller.sendBack(false, RefreshStatus.idle);
        pageIndex += 1;
        setState(() {});
      }
     
      @override
      void querySamplesFailed() {
        //查询失败,修改页面状态
        _controller.sendBack(false, RefreshStatus.idle);
        loadStatus = PageLoadStatus.failed;
        setState(() {});
      }
     
      @override
      void queryCartsSampleCountSuccess(int count) {
        setState(() {
          samplesCount = count;
        });
      }
     
      @override
      void doJoinCartsFailed(String message) {
        Fluttertoast.showToast(msg: message);
      }
     
      @override
      void doJoinCartsSuccess(String message) {
        DialogUtil.showTips(context, text: message);
        _presenter.querySamples(pageIndex = 1);
        _presenter.queryCartsSampleCount();
      }
    }

    经过改动之后,会发现程序的类变多了,代码量视乎更大了。但是我们并不是以代码量的多少来评价代码的质量,往往是以代码的可阅读性和可变性来评价。经过改动之后,SampleClaimPage 类主要负责UI的实现和UI与数据的绑定及交互。ClaimPresenter类主要负责业务逻辑的实现和数据与UI之间交互的建立。而ClaimModel 类只需要简单的实现数据的获取。代码逻辑变得十分清晰。

    结束语
    代码模式的设计需要便于程序员理解代码,mvp模式特别适用于页面逻辑较为复杂的情况。当页面逻辑十分简单只时,就无需为了设计而设计,也就是代码界的一句金玉良言:“不要过度设计”。欢迎大家指正。

  • 相关阅读:
    可视化工具D3.js教程 入门 (第十三章)—— 树状图
    可视化工具D3.js教程 入门 (第十二章)—— 力导向图
    可视化工具D3.js教程 入门 (第十一章)—— 饼图
    可视化工具D3.js教程 入门 (第十章)—— 交互式操作
    vue滑动页面选中标题,选中标题滚动到指定区域
    Vue样式穿透
    操作系统:进程和线程+进程的通讯方式
    客户端与服务端长连接的几种方式
    前端性能优化的 24 条建议(2020)-收藏
    idea中修改git提交代码的用户名
  • 原文地址:https://www.cnblogs.com/zhujiabin/p/10337439.html
Copyright © 2011-2022 走看看