To the point

When we first learned flutter, we probably wrote the UI layer and the business logic layer together. As the DART file got bigger and bigger, the logic inside became more and more complex. Then we thought, shouldn’t we refactor the code? First of all, the code should have a single responsibility as far as possible, so that it is easy to modify if there is a problem, and it will not affect the whole body. When developing Android, I have used MVP and MVVM, and PERSONALLY I prefer MVVM. To talk about the difference between the two, first of all, MVP mode is when you get the data, you need to control how to refresh the UI. MVVM binds the data to the UI, and when your data changes, the UI itself changes. BLoC is actually an MVVM framework, and this article will teach you how to implement your own MVVM framework. In fact, BLoC and Google Provide, etc., are the same in core, and they both obtain the required data by searching the upper node.

Then we will talk about how to implement our own MVVM framework in Flutter to decoupled the UI layer from the business logic layer. We will first look at some code that does not use the MVVM design pattern. All code has been uploaded to Github main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MVVM Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: HomePageNoMVVM(), ); }}Copy the code

page_home_no_mvvm.dart

Liuhc Class HomePageNoMVVM extends StatefulWidget {@override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePageNoMVVM> { bool _loading =true;
  String _text;

  @override
  void initState() {
    super.initState();
    loadData();
  }

  void loadData() {
    NetWork.query().then((String text) {
      setState(() {
        _loading = false;
        _text = text;
      });
    }).catchError((error) {
      setState(() {
        _loading = false;
        _text = error.toString();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("A sample of Flutter without MVVM"),
      ),
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text("Click to retrieve web data"), onPressed: () { loadData(); }, ), Offstage( offstage: ! _loading, child: CircularProgressIndicator(), ), Expanded( child: SingleChildScrollView( child: Text("${_text ?? ""}"(() [() [() [() [() [() [() }}Copy the code

As you can see, enter the page, we need to request data, access to the data, we’ll call setState refresh the page, and then display the access to the data, the code function is normal, but the code is not elegant, because the UI layer was the need to control how to display the UI, and need to deal with the business layer, Getting data from the business layer and then updating the UI yourself is a clear violation of the single responsibility principle, and as this logic becomes more and more difficult to maintain later, let’s see how we can implement the MVVM framework ourselves to refactor this code.

1. First we create a base class for the ViewModel

abstract_base_viewmodel.dart

import 'package:flutter/widgets.dart'; Author: liuHC Abstract class BaseViewModel {bool _isFirst =true;

  bool get isFirst=>_isFirst;

  @mustCallSuper
  void init(BuildContext context) {
    if (_isFirst) {
      _isFirst = false;
      doInit(context); }} @protected Future refreshData(BuildContext context); @protected voiddoInit(BuildContext context);

  void dispose();
}
Copy the code

So in this class, I’ve encapsulated some of the methods that basically all viewModels need, and the whole point of that init method is to make sure that doInit is only executed once, so it doesn’t have to do that for all of your subclasses to make sure that they’ve already done init, they just have to override doInit to make sure that the code in that method is only executed once.

2. Next, we create a Widget with an instance of the class property ViewModel

viewmodel_provider.dart

import 'package:flutter/material.dart';
import 'package:flutter_mvvm/core/abstract_base_viewmodel.dart'; Liuhc Class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {final T viewModel; final Widget child; ViewModelProvider({ @required this.viewModel, @required this.child, }); static T of<T extends BaseViewModel>(BuildContext context) { finaltype = _typeOf<ViewModelProvider<T>>();
    ViewModelProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.viewModel;
  }

  static Type _typeOf<T>() => T;

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

class _ViewModelProviderState extends State<ViewModelProvider> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

  @override
  void dispose() { widget.viewModel.dispose(); super.dispose(); }}Copy the code

3. Complete

Yes, it’s that simple. We created two classes and completed our framework for the MVVM design pattern

Use 4.

Now let’s see, how can we use this MVVM framework to refactor the code we just wrote

4.1 First write our ViewModel class. Here I use RxDart. The BehaviorSubject is a BehaviorSubject that saves the last data sent, but you don’t use this feature here

viewmodel_home.dart

import 'package:flutter/material.dart';
import 'package:flutter_mvvm/core/abstract_base_viewmodel.dart';
import 'package:flutter_mvvm/core/network.dart';
import 'package:rxdart/rxdart.dart'; Liuhc HomeViewModel extends BaseViewModel {liuHC HomeViewModel extends BaseViewModel; close_sinks BehaviorSubject<String> _dataObservable = BehaviorSubject(); Stream<String> get dataStream => _dataObservable.stream; @override voiddispose() {
    _dataObservable.close();
  }

  @override
  void doInit(BuildContext context) { refreshData(context); } override Future refreshData(BuildContext context) {override Future refreshData(BuildContext context) {override Future refreshData(BuildContext context) {override Future refreshData(BuildContext context) {override Future refreshData(BuildContext context)returnNetWork.query().then((String text) { _dataObservable.add(text); }).catchError((error) { _dataObservable.addError(error); }); }}Copy the code

4.2 Then let’s refactor the home page Widget

page_home.dart

import 'package:flutter/material.dart';
import 'package:flutter_mvvm/core/viewmodel_provider.dart';
import 'package:flutter_mvvm/page/home/viewmodel_home.dart'; // Author :liuhc class HomePage extends StatefulWidget {@override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { HomeViewModel _viewModel; @override voidinitState() {
    super.initState();
    _viewModel = ViewModelProvider.of(context);
    _viewModel.init(context);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("An example of Flutter using MVVM"),
      ),
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text("Click to retrieve web data"),
              onPressed: () {
                _viewModel.refreshData(context);
              },
            ),
            Expanded(
              child: SingleChildScrollView(
                child: StreamBuilder(
                  stream: _viewModel.dataStream,
                  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                    if (snapshot.connectionState == ConnectionState.waiting) {
                      return Center(
                        child: CircularProgressIndicator(),
                      );
                    }
                    return Text(
                      "${snapshot.hasError ? snapshot.error : snapshot.data}",); }, (), [[, (), (), (); }}Copy the code

The key part of the code above is through viewModelProvider.of (context); This section is not the end of this article. If you don’t know this section, please check it out for yourself.

4.3 Then we modify the program entry, see how toHome page WidgetandThe homepage the ViewModelBound together

main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MVVM Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: ViewModelProvider( viewModel: HomeViewModel(), child: HomePage(), ), ); }}Copy the code

In this code, our home does not pass a HomePage() directly. Instead, we pass a ViewModelProvider into our HomePage(). We save the viewModel instance, and in the Build method of the ViewModelProvider, we return the child directly, We also define a method static T of

(BuildContext Context) In this method by calling context. AncestorWidgetOfExactType found in the viewModel class attribute, so we found inside the _HomePageState class incoming ViewModelProvider viewModel, You can then use the viewModel for the next step.

Article to this end, in the use of the method in the process of development, also can perfectly solve the problem of TabView every TAB click on an error (with all know what I’m saying), because even if use the AutomaticKeepAliveClientMixin, each click TAB if no problem, I’ve tried a lot of methods and they don’t work, but this works because the StreamBuilder is used to refresh the data and the ViewModel is stored in the upper widget. So when this widget is redrawn, the viewModel instance of the upper widget will not change, and the data will still be in the Stream. Therefore, even if the build method is re-executed, there will be no network request for data again, except when we manually add data to the StreamController. The new data is given to the widget to redraw.