0. Frame history

MVC

MVC can be said to be a classic framework, but in the practice of MVC framework, it is difficult for us to reduce its coupling degree. In the process of use, a large number of interfaces will appear in controller, resulting in the code in Controller is very large, and when implemented in View, We’re used to implementing only things related to page layout, and when it comes to animation, page layout logic, we’re going to dump it into the Controller. The complex logic of controller and the extremely high degree of coupling between the page make it impossible for us to separate the test code in the development process, so we can only carry out full test in the way of E2E, increasing the workload of self-testing for programmers.

MVVM

The MVVM architecture is currently the newest in the MVX hierarchy, and let’s hope it takes into account the problems that the MVX pattern has encountered before. From a front-end perspective, MVVM is a very familiar framework. After all, react/ VUE is based on MVVM framework. The biggest transformation of MVVM for MVC is to disassemble controller and divide it into view and View-Model parts. Rendering pages in a data-driven way is more intuitive.

The MVVM features:
  • The MVVM architecture treats the ViewController as a View.
  • There is no tight coupling between the View and Model

9 framework

The VIPER framework can be said to divide the hierarchy to the smallest possible level. The natural decoupling makes testing VIPER code extremely easy. Views are associated with the Router, and no pages are strongly dependent on each other, which means you can test a single page without having to run the entire process backwards. Furthermore, each component generated by the Viper framework can be considered as a separate module, a separate entity, and as long as your infrastructure is the same, these separate modules can be nested within each other in any system, rather than having to do the same work to develop these components separately.

1. What is the VIPER framework

The VIPER framework was originally designed for iOS and evolved from the MVVM framework.

A VIPER is a View Interactor Presenter Entity Router. VIPER iterates on the level of responsibility division, which is divided into five levels:

  • Presentator — contains business logic at the UI level and method calls at the interaction level.
  • Interactionators – includes business logic about data and network requests, such as creating an entity (data), or fetching some data from a server. To implement these functions, services and managers are used, but they are not considered modules within the VIPER architecture, but external dependencies.
  • Entities – Generic data objects that are not part of the data access hierarchy because data access is the responsibility of the interactor.
  • Routing – Used to connect VIPER modules.

Fully decoupled VIPER frame diagram:

Wherein the VIPER framework event is subdivided:

2. Advantages and disadvantages of using the VIPER framework

advantages

VIPER is characterized by clear responsibilities, fine granularity and clear isolation relationships, which can bring many advantages:

  • Good testability. UI testing and business logic testing can be done separately.
  • Easy to iterate. Each part follows a single responsibility, and it’s clear where the new code should go.
  • High degree of isolation, natural decoupling. One module’s code does not easily affect another.
  • Easy to work in a team. Clear division of labor in each part, easy to unify the code style in team cooperation, can quickly take over other people’s code.

disadvantages

Due to the fine granularity of requirements, VIPER will bring the following problems correspondingly:

  • The larger the number of classes in a module, the larger the amount of code, the more time it takes to design interfaces between layers. Using code templates to automatically generate files and template code can save a lot of rework, while the time spent designing and writing interfaces is inevitable on the way to reducing coupling. You can also use techniques like data binding to reduce some of the layers of delivery.
  • Initialization of a module is complicated. To open a new interface, you need to generate a View, Presenter, and Interactor, and set up dependencies among them.

3. Disassembly and practice in Flutter

The most critical thing of VIPER framework is how to define the relevant interfaces. In order to realize the directory structure of VIPER framework, we implemented the code as the following directory structure:

Directory structure:


  • Dart is the entry file
  • Router indicates a unified route configuration file
  • The BaseClasses is a virtual class that the VIPER framework needs to implement
  • MainTab is the page used for this experiment

Code diagram:

The View:

View is used to initialize the current page and pass page events to its Presenter


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

  final MainTabPresenter presenter;

  // Use appBar in mainTab
  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: constBoxDecoration(), controller: tabController, tabs: createTabs(), ), ), ), ), ); }}Copy the code

Interactor:

Interactor instantiates the relevant data and provides the data interface to the Presenter for use by the View:


class MainTabViewModel {
  List<TabModel> tabs;

  MainTabViewModel({
    this.tabs,
  });
}

class MainTabInteractor implements BaseInteractor {
  MainTabViewModel viewModel = MainTabViewModel(
    tabs: [
      TabModel(
        tabName: 'test tab1',
        body: Container(
          child: Text('Test page 1'),),),... ] ,); }Copy the code

Presenter:

Presenter mainly feeds the viewModel processed in Interactor back to the View and receives the page events in the View for processing.

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'); }}Copy the code

The Entity:

Entity mainly implements the various class definitions needed in the current structure, and does not need to be materialized

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

  TabModel({
    this.tabName,
    this.icon,
    this.body,
  });
}
Copy the code

The Router:

The Router defines push/ POP actions and how the page is initialized. Page initialization is triggered by Presenter.

class MainTabRouter extends BaseRouter {
  @override
  void push(context, params, title) {
    super.push(context, params, title);
    Route route = MaterialPageRoute(builder: (context) {
      returnMainTabPresenter().create(params); }); Navigator.push(context, route); }}Copy the code

After the above code logic is implemented:

We implement the static method Push/Pop in the main route:

// Define the key value of the Router for subsequent calls
enum RouterKey {
  MainTab,
}

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

  static void push(RouterKey destination, context, {params, title}) {
    if (routeMap.containsKey(destination)) {
      varrouter = routeMap[destination]; router.push(context, params, title); }}static void pop(context) {
    if(Navigator.canPop(context)) { Navigator.pop(context); }}}Copy the code

At this point, a complete set of VIPER process is completed. At this point, a Button is written in main to trigger the Router’s page push effect:

body: Center(
  child: MaterialButton(
    onPressed: () {
      Router.push("mainTab", context);
    },
    child: Text('push page'),),),Copy the code

Then you can see the complete flow:

4. Follow-up optimization

1. Add page creation scripts/plug-ins to quickly generate frame pages 2. Remove base classes for use in other projects

5. Code repository

Github.com/owops/Flutt…