Flutter VIPER架构-解决复用和测试问题的利器

0.框架历史

MVC

MVC能够说是框架的经典了,可是在MVC框架的实践中,咱们很难作到下降它的耦合度,咱们在使用过程当中,会有大量的接口都出如今controller中,致使controller中的代码很是的庞大,而在view中实现的时候,咱们又习惯性的只实现页面布局相关的东西,而到了动画,页面布局逻辑,咱们又会丢到controller中去处理。controller复杂的逻辑,与页面极高的耦合度,会致使咱们在开发过程没法抽离测试代码,只能经过e2e的方式进行全量测试,增长程序员自测的工做量。 前端

MVVM

MVVM架构是MVX里面目前来讲最新的一个,让咱们但愿它在出现的时候已经考虑到了MVX模式以前所遇到的问题吧。
在一个前端的角度来说,MVVM是一个再熟悉不过的框架了,毕竟react/vue都是在MVVM框架的基础上出现的,MVVM对于MVC来讲作的最大的改造就是将controller拆解,并分给view和view-model两个部分,经过数据驱动的方式呈现页面,更加的直观。 vue

MVVM 特色:
  • MVVM 架构把 ViewController 看作 View。
  • View 和 Model 之间没有紧耦合

VIPER 框架

VIPER 框架,能够说把层次划分到最细,自然的解耦让VIPER代码的测试工做变得异常轻松。
view与view之间是经过router相关联的,没有任何页面之间是强依赖的,这意味着你能够单独测试某张页面而不须要将所有的流程都回归一遍。
并且,viper框架生成的各个组件,均可以认为是一个独立的模块,一个独立的个体,只要你的基础架构相同,那么这些独立模块在任何系统中均可以互相嵌套使用,而不须要作重复工做单独开发这些组件。react

1.了解什么是VIPER框架

VIPER框架最初起始于iOS设计中,是在MVVM框架的基础上演变而来。git

从字面意思来理解,VIPER 即 View Interactor Presenter Entity Router(视图 交互 协调器 实体 路由)。VIPER 在责任划分层面进行了迭代,VIPER 分为五个层次:程序员

  • 展现器 -- 包含 UI 层面的业务逻辑以及在交互器层面的方法调用。
  • 交互器 -- 包括关于数据和网络请求的业务逻辑,例如建立一个实体(数据),或者从服务器中获取一些数据。为了实现这些功能,须要使用服务、管理器,可是他们并不被认为是 VIPER 架构内的模块,而是外部依赖。
  • 实体 -- 普通的数据对象,不属于数据访问层次,由于数据访问属于交互器的职责。
  • 路由 -- 用来链接 VIPER 的各个模块。

彻底解耦的VIPER框架图:

其中VIPER框架事件细分:

2.使用VIPER框架的优劣势

优势

VIPER的特点就是职责明确,粒度细,隔离关系明确,这样能带来不少优势:github

  • 可测试性好。UI测试和业务逻辑测试能够各自单独进行。
  • 易于迭代。各部分遵循单一职责,能够很明确地知道新的代码应该放在哪里。
  • 隔离程度高,自然解耦。一个模块的代码不容易影响到另外一个模块。
  • 易于团队合做。各部分分工明确,团队合做时易于统一代码风格,能够快速接手别人的代码。

缺点

VIPER由于需求的拆分粒度细,相应的会带来如下问题:服务器

  • 一个模块内的类数量增大,代码量增大,在层与层之间须要花更多时间设计接口。使用代码模板来自动生成文件和模板代码能够减小不少重复劳动,而花费时间设计和编写接口是减小耦合的路上不可避免的,你也可使用数据绑定这样的技术来减小一些传递的层次。
  • 模块的初始化较为复杂,打开一个新的界面须要生成View、Presenter、Interactor,而且设置互相之间的依赖关系。

3.在Flutter中的拆解与实践

VIPER框架最关键的是如何将相关接口定义出来,为了实现VIPER框架的目录结构,咱们将代码实现为以下目录结构: 网络

目录结构:


目录结构中:

  • main.dart为入口文件
  • Router为统一的路由配置文件
  • BaseClasses为VIPER框架所须要实现的虚拟类
  • MainTab为此次实验所使用的页面

代码示意:

View:

View中主要是当前页面的初始化等操做,并将页面事件传递给本身的Presenter架构

class MainTabView extends StatefulWidget implements BaseView {
  const MainTabView({
    Key key,
    this.appBar,
    this.views,
    this.presenter,
  });

  final MainTabPresenter presenter;

  // mainTab中的appBar使用
  final PreferredSizeWidget appBar;

  final List<TabModel> views;

  @override
  _MainTabViewState createState() => _MainTabViewState();
}

class _MainTabViewState extends State<MainTabView> with SingleTickerProviderStateMixin {
  TabController tabController;

  @override
  void initState() {
    super.initState();
    tabController = new TabController(length: widget.views.length, vsync: this);
  }

  @override
  void dispose() {
    super.dispose();
    tabController.dispose();
  }

  List<Tab> createTabs() {
    List<Tab> tabs = new List<Tab>();
    widget.views.forEach((e) {
      var tab = Tab(
        text: e.tabName,
        icon: e.icon,
      );
      tabs.add(tab);
    });
    return tabs;
  }

  List<Widget> createBody() {
    List<Widget> bodies = new List<Widget>();
    widget.views.forEach((e) {
      bodies.add(e.body);
    });
    return bodies;
  }

  @override
  Widget build(BuildContext context) {
    print(widget.views.map((e) => e.body));
    return Scaffold(
      backgroundColor: Colors.blue,
      appBar: widget.appBar,
      body: Material(
        child: TabBarView(
          controller: tabController,
          children: createBody(),
        ),
      ),
      bottomNavigationBar: SafeArea(
        child: Material(
          color: Colors.blue,
          child: SafeArea(
            child: TabBar(
              onTap: (index) {
                widget.presenter.tabChanged(index);
              },
              indicator: const BoxDecoration(),
              controller: tabController,
              tabs: createTabs(),
            ),
          ),
        ),
      ),
    );
  }
}

复制代码

Interactor:

Interactor中主要是实例化相关的数据,并将数据接口提供给Presenter以反馈给View使用:app

class MainTabViewModel {
  List<TabModel> tabs;

  MainTabViewModel({
    this.tabs,
  });
}

class MainTabInteractor implements BaseInteractor {
  MainTabViewModel viewModel = MainTabViewModel(
    tabs: [
      TabModel(
        tabName: '测试tab1',
        body: Container(
          child: Text('测试页面1'),
        ),
      ),
      ...
    ],
  );
}

复制代码

Presenter:

Presenter主要是将Interactor中处理的viewModel反馈给View,并接收View中的页面事件,进行处理。

class MainTabPresenter implements BasePresenter {
  @override
  Widget create(List<TabModel> params) {
    return MainTabView(
      views: MainTabInteractor().viewModel.tabs,
      presenter: this,
    );
  }

  void tabChanged(int index) {
    print('tab changed to: $index');
  }
}
复制代码

Entity:

Entity中主要是实现当前结构中所须要使用的各类类定义,并不须要作实体化操做

class TabModel implements BaseModel {
  String tabName;
  Icon icon;
  Widget body;

  TabModel({
    this.tabName,
    this.icon,
    this.body,
  });
}
复制代码

Router:

Router中主要定义push/pop操做时的一些动做,以及页面如何初始化。页面初始化均由Presenter触发。

class MainTabRouter extends BaseRouter {
  @override
  void push(context, params, title) {
    super.push(context, params, title);
    Route route = MaterialPageRoute(builder: (context) {
      return MainTabPresenter().create(params);
    });
    Navigator.push(context, route);
  }
}
复制代码

在上述代码逻辑实现后:

咱们在主路由中实现静态方法Push/Pop:

// 定义Router的key值,方便后续调用
enum RouterKey {
  MainTab,
}

// 实现Router类
class Router {
  static Map<RouterKey, BaseRouter> routeMap = {
    RouterKey.MainTab: MainTabRouter(),
  };

  static void push(RouterKey destination, context, {params, title}) {
    if (routeMap.containsKey(destination)) {
      var router = routeMap[destination];
      router.push(context, params, title);
    }
  }

  static void pop(context) {
    if (Navigator.canPop(context)) {
      Navigator.pop(context);
    }
  }
}

复制代码

此时咱们的一套完整的VIPER流程就实现完成了
此时经过main中写入一个Button,用来触发Router的页面push效果:

body: Center(
  child: MaterialButton(
    onPressed: () {
      Router.push("mainTab", context);
    },
    child: Text('push页面'),
  ),
),
复制代码

以后就能够看到完整的一套流程了:

4.后续优化

1.增长页面建立脚本/插件,用于快速生成框架页面
2.抽离基类,以便于其余项目中使用

5.代码仓库

github.com/owops/Flutt…

相关文章
相关标签/搜索