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框架的目錄結構,咱們將代碼實現爲以下目錄結構: markdown

目錄結構:


目錄結構中:

  • 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使用:架構

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…

相關文章
相關標籤/搜索