【跨平台开发Flutter】Flutter里的MVVM

目录
一、最原始的MVVM
二、使用Listener的MVVM
三、使用Provider的MVVM
四、使用GetX的MVVM

需求很简单:

  • 搜索框输入“张”时就请求前缀为“张”的用户数据、同时也请求后缀为“张”的用户数据,搜索框输入“李”时就请求前缀为“李”的用户数据、同时也请求后缀为“李”的用户数据
  • 请求完成后,前缀数据交给上面的ListView显示,后缀数据交给下面的ListView显示


一、最原始的MVVM


  • Model层
-----------person_model.dart-----------

/*
 Model的职责:Model只负责封装数据,不做任何其它操作。
 */

/// Person模型
class PersonModel {
  /// 普通构造方法
  PersonModel({
    this.name,
    this.sex,
    this.age,
  });

  /// 姓名
  String? name;

  /// 性别
  ///
  /// 0-未知,1-男,2-女
  int? sex;

  /// 年龄
  int? age;

  /// 工厂构造方法
  factory PersonModel.fromJson(Map<String, dynamic> json) => PersonModel(
        name: json["name"] == null ? null : json["name"],
        sex: json["sex"] == null ? null : json["sex"],
        age: json["age"] == null ? null : json["age"],
      );

  /// 模型转字典
  Map<String, dynamic> toJson() => {
        "name": name == null ? null : name,
        "sex": sex == null ? null : sex,
        "age": age == null ? null : age,
      };
}
  • View层
-----------search_bar_widget.dart-----------

/*
 View的职责:View负责响应与业务有关的事件并交给Controller去处理,怎么交给Controller呢?通过闭包、通知等。
 */

import 'package:flutter/material.dart';

/// 搜索框Widget
class SearchBarWidget extends StatelessWidget {
  SearchBarWidget({
    this.searchTextDidChangeCallback,
  });

  /// 搜索内容改变的回调
  final void Function(String text)? searchTextDidChangeCallback;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      child: TextField(
        textInputAction: TextInputAction.done,
        onSubmitted: (text) {
          if (searchTextDidChangeCallback != null) {
            searchTextDidChangeCallback!(text);
          }
        },
      ),
    );
  }
}
-----------prefix_list_view_widget.dart-----------

/*
 View的职责:View负责显示数据,那怎么显示数据呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 前缀列表Widget
class PrefixListViewWidget extends StatelessWidget {
  PrefixListViewWidget({
    required this.personViewModel,
  });

  final PersonViewModel personViewModel;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: ListView.builder(
        itemCount: personViewModel.prefixPersonViewModelList.length,
        itemBuilder: (context, index) {
          PersonViewModel personVM =
              personViewModel.prefixPersonViewModelList[index];

          return Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Text(personVM.name),
              Text(personVM.sex),
              Text("${personVM.age}"),
            ],
          );
        },
        itemExtent: 100,
      ),
    );
  }
}
-----------suffix_list_view_widget.dart-----------

/*
 View的职责:View负责显示数据,那怎么显示数据呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 后缀列表Widget
class SuffixListViewWidget extends StatelessWidget {
  SuffixListViewWidget({
    required this.personViewModel,
  });

  final PersonViewModel personViewModel;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: ListView.builder(
        itemCount: personViewModel.suffixPersonViewModelList.length,
        itemBuilder: (context, index) {
          PersonViewModel personVM =
              personViewModel.suffixPersonViewModelList[index];

          return Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Text(personVM.name),
              Text(personVM.sex),
              Text("${personVM.age}"),
            ],
          );
        },
        itemExtent: 100,
      ),
    );
  }
}
  • ViewModel层
-----------base_view_model.dart-----------

import 'package:flutter/cupertino.dart';

enum ViewState {
  loading, // 加载中
  success, // 加载成功
  empty, // 加载成功,但数据为空
  failure, // 加载失败
  noNetwork, // 没网
}

class BaseViewModel extends ChangeNotifier {
  ViewState _state = ViewState.success;

  ViewState get state => _state;

  set state(ViewState value) {
    _state = value;
    notifyListeners();
  }
}
-----------person_view_model.dart-----------

/*
 ViewModel的职责:
  1、ViewModel负责获取数据;
  2、ViewModel负责处理数据;
  3、ViewModel负责存储数据。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// Person视图模型
class PersonViewModel extends BaseViewModel {
  // 命名构造方法,专门用来初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一个_personModel,以便处理数据:ViewModel一对一Model地添加属性并处理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通构造方法
  PersonViewModel();

  /// 存储数据:vm数组
  ///
  /// 真正暴露给外面使用的是vm数组,里面的数据已经处理好了,直接拿着显示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存储数据:错误消息
  String errorMsg = "";

  /// 处理数据:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 处理数据:性别
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 处理数据:年龄
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 请求前缀数据
  Future<void> loadPrefixData(
      String params, void Function(bool isSuccess) completionHandler) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      completionHandler(true);
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      completionHandler(false);
    }
  }

  /// 请求后缀数据
  Future<void> loadSuffixData(
      String params, void Function(bool isSuccess) completionHandler) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      completionHandler(true);
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      completionHandler(false);
    }
  }
}
  • Controller层
-----------list_page.dart-----------

/*
 Controller的职责:
  1、Controller负责持有View,创建View,并把View添加到窗口上显示;
  2、Controller负责持有ViewModel,调用ViewModel的方法去请求数据;
  3、vm --> view:Controller调用vm的方法请求数据,请求完成后vm是通过回调的方式告诉Controller的:
    请求成功后Controller需要调用一下setState来刷一下UI,这样view就会去拿vm里最新存储的数据来展示了
    请求失败后Controller可以toast一下错误信息给用户看,或者调用一下setState来刷一下UI,刷成暂无数据那种view
  4、view --> vm:view产生的变化是通过回调告诉Controller的,Controller可以调用vm的方法把view发生的变化告诉它
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatefulWidget {
  const ListPage({Key? key}) : super(key: key);

  @override
  State<ListPage> createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> {
  PersonViewModel _personViewModel = PersonViewModel();

  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildPrefixListViewWidget(),
            ),
            Expanded(
              child: _buildSuffixListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    return Container(
      color: Colors.red,
      child: SearchBarWidget(
        searchTextDidChangeCallback: (text) {
          _personViewModel.loadPrefixData(text, (isSuccess) {
            if (mounted) {
              // 注意要判断一下当前界面还在不在视图树中,因为请求都是异步的,很有可能请求还没完成我们就退出界面了,退出界面时我们什么都不做即可
              if (isSuccess) {
                setState(() {});
              } else {
                print(_personViewModel.errorMsg);
              }
            }
          });

          _personViewModel.loadSuffixData(text, (isSuccess) {
            if (mounted) {
              // 注意要判断一下当前界面还在不在视图树中,因为请求都是异步的,很有可能请求还没完成我们就退出界面了,退出界面时我们什么都不做即可
              if (isSuccess) {
                setState(() {});
              } else {
                print(_personViewModel.errorMsg);
              }
            }
          });
        },
      ),
    );
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget(
      personViewModel: _personViewModel,
    );
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return SuffixListViewWidget(
      personViewModel: _personViewModel,
    );
  }
}


二、使用Listener的MVVM


相对于最原始的MVVM来说,它的变化其实就是【Controller调用vm的方法请求数据,请求完成后vm通过什么方式告诉Controller,之前是通过回调的方式,现在是通过Listener的方式】。

它的优势就是【界面退出时假如我们的请求还没走完,会自动移除监听】,也就是说就算网络请求完了也不会再触发Controller里的监听了,我们压根儿不需要关心这个界面在不在视图树里,只需要做业务即可。【它的劣势就是因为所有的业务都会触发同一个监听而使得代码判断起来复杂了,之前起码是一个业务一个回调、各管各的】,它给人感觉上好像是在数据驱动UI,但其实并不是,因为这里并不存在数据和UI的绑定操作,本质上还是数据变化后、我们手动刷新UI来展示最新的数据。

因此相对于最原始的MVVM来说,更推荐最原始的MVVM。

  • Model层和View层都不需要改动
  • ViewModel层
-----------base_view_model.dart-----------

import 'package:flutter/cupertino.dart';

enum ViewState {
  loading, // 加载中
  success, // 加载成功
  empty, // 加载成功,但数据为空
  failure, // 加载失败
  noNetwork, // 没网
}

class BaseViewModel extends ChangeNotifier {
  ViewState _state = ViewState.success;

  ViewState get state => _state;

  set state(ViewState value) {
    _state = value;
    notifyListeners();
  }
}
-----------person_view_model.dart-----------

/*
 ViewModel的职责:
  1、ViewModel负责获取数据;
  2、ViewModel负责处理数据;
  3、ViewModel负责存储数据。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// PersonViewModel里所做的业务
class PersonViewModelService {
  static const String loadPrefixData = "loadPrefixData";
  static const String loadSuffixData = "loadSuffixData";
}

/// PersonViewModel里所做业务的结果
class PersonViewModelServiceResult {
  PersonViewModelServiceResult({
    required this.service,
    required this.isSuccess,
  });

  /// 具体是哪个业务
  String service;

  /// 结果
  bool isSuccess;
}

/// Person视图模型
///
/// 要想使用Listener,PersonViewModel得继承自或者混入ChangeNotifier
class PersonViewModel extends BaseViewModel {
  late PersonViewModelServiceResult serviceResult;

  // 命名构造方法,专门用来初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一个_personModel,以便处理数据:ViewModel一对一Model地添加属性并处理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通构造方法
  PersonViewModel();

  /// 存储数据:vm数组
  ///
  /// 真正暴露给外面使用的是vm数组,里面的数据已经处理好了,直接拿着显示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存储数据:错误消息
  String errorMsg = "";

  /// 处理数据:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 处理数据:性别
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 处理数据:年龄
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 请求前缀数据
  ///
  /// 请求完成后,本来是通过回调告诉Controller的,现在不要回调了,通过Listener告诉Controller
  Future<void> loadPrefixData(String params) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadPrefixData, isSuccess: true);
      notifyListeners();
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadPrefixData, isSuccess: false);
      notifyListeners();
    }
  }

  /// 请求后缀数据
  ///
  /// 请求完成后,本来是通过回调告诉Controller的,现在不要回调了,通过Listener告诉Controller
  Future<void> loadSuffixData(String params) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      state = ViewState.success;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadSuffixData, isSuccess: true);
      notifyListeners();
    } catch (error) {
      errorMsg = error.toString();

      state = ViewState.failure;
      serviceResult = PersonViewModelServiceResult(
          service: PersonViewModelService.loadSuffixData, isSuccess: false);
      notifyListeners();
    }
  }
}
  • Controller层
-----------list_page.dart-----------

/*
 Controller的职责:
  1、Controller负责持有View,创建View,并把View添加到窗口上显示;
  2、Controller负责持有ViewModel,调用ViewModel的方法去请求数据;
  3、vm --> view:Controller调用vm的方法请求数据,请求完成后vm是通过Listener而非回调的方式告诉Controller的:
    请求成功后Controller需要调用一下setState来刷一下UI,这样view就会去拿vm里最新存储的数据来展示了
    请求失败后Controller可以toast一下错误信息给用户看,或者调用一下setState来刷一下UI,刷成暂无数据那种view
  4、view --> vm:view产生的变化是通过回调告诉Controller的,Controller可以调用vm的方法把view发生的变化告诉它
 */

import 'package:flutter/material.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatefulWidget {
  const ListPage({Key? key}) : super(key: key);

  @override
  State<ListPage> createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> {
  PersonViewModel _personViewModel = PersonViewModel();

  @override
  void initState() {
    // 第一步:_personViewModel添加监听
    //
    // 它里面任何时候、任何地方发出notifyListeners,都会触发这里添加好的监听
    // 那它里面什么时候、什么地方发出notifyListeners呢?当然就是请求完成的时候,在请求完成的回调里发出
    // 当然因为它内部可能会做多个业务,如果多个业务都发出了notifyListeners,则都会触发这里的同一个回调,因此我们会在它里面添加一个类来区分到底是哪个业务完成了,以便在监听里处理不同的业务
    _personViewModel.addListener(_personViewModelListener);

    super.initState();
  }

  // 第二步:_personViewModel处理监听
  void _personViewModelListener() {
    if (_personViewModel.serviceResult.service ==
        PersonViewModelService.loadPrefixData) {
      if (_personViewModel.serviceResult.isSuccess) {
        setState(() {});
      } else {
        print(_personViewModel.errorMsg);
      }
    } else if (_personViewModel.serviceResult.service ==
        PersonViewModelService.loadSuffixData) {
      if (_personViewModel.serviceResult.isSuccess) {
        setState(() {});
      } else {
        print(_personViewModel.errorMsg);
      }
    }
  }

  @override
  void dispose() {
    // 第三步:_personViewModel移除监听
    _personViewModel.removeListener(_personViewModelListener);

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildPrefixListViewWidget(),
            ),
            Expanded(
              child: _buildSuffixListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    return SearchBarWidget(
      searchTextDidChangeCallback: (text) {
        _personViewModel.loadPrefixData(text);
        _personViewModel.loadSuffixData(text);
      },
    );
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget(
      personViewModel: _personViewModel,
    );
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildSuffixListViewWidget");

    return SuffixListViewWidget(
      personViewModel: _personViewModel,
    );
  }
}


三、使用Provider的MVVM


首先我们要明白Provider这个框架的首要功能是数据共享,所以我们这里使用Provider的首要目的就是用它来共享_personViewModel这个数据,因为我们的ListPage、PrefixListViewWidget和SuffixListViewWidget都使用到了同一个_personViewModel,当然我们的例子可能比较简单,【实际开发中你的一个界面里可能会由很多很多个Widget组成,而它们可能都需要使用同一个viewModel,甚至多个界面之间也需要使用同一个viewModel,那最原始的写法就是像最原始的MVVM里那样通过指针传递viewModel来共享,而Provider则提供了另外一种共享数据的方式——只要一堆Widget拥有同一个Provider作为父视图,那么这些Widget就都可以共享这个Provider绑定的viewModel数据】,这就是它相对于最原始MVVM的第一个变化,这个变化主要解决了viewModel传来传去的问题。(如果你只想一个界面内的多个Widget共享数据,那么这多个Widget的父视图就必须得是这个Provider,所以你可以把Provider包在这个界面上即可;如果你想多个界面之间共享数据,那么这多个界面的父视图就必须得是这个Provider,因此这个时候我们会把Provider包在App的最底层)

实现了上面的内容之后其实已经完成了Provider数据共享的功能,【但是我们会发现使用Provider时,它要求我们的ViewModel必须继承自或混入ChangeNotifier,这是因为Provider的第二个功能就是局部刷新,也就是说我们只需要在ViewModel里合适的时机、合适的地方发出一个notifyListeners,Provider就会自动触发它Consumer或Selector的回调来只刷新局部UI来展示最新的数据、当然我们也可以在些回调里Toast错误消息等。同时我们也只需要在ViewModel里发notifyListeners就行了,也不用像使用Listener那样考虑到底是什么业务完成了而做一堆判断,Controller里也没有什么监听,因为各个监听已经分散到了各个Consumer或Selector回调那里】,这就是它相对于最原始MVVM的第二个变化,这个变化主要解决了ViewModel怎么把变化告诉Controller的问题。

【它的优势就是更加简单的数据共享方式 + 响应式编程数据驱动UI。】【它的劣势就是Provider框架的侵入性太强了,而且代码编写起来有点费劲。】

因此相对于最原始的MVVM来说,你有余力的话可以学学Provider并应用在你的MVVM中。

  • App最底层
-----------main.dart-----------

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:flutter_mvvm/classes/page/list_page.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

void main() {
  // 第一步:创建数据存放地,也就是我们想共享的view_model———即person_view_model.dart
  // 第二步:在App的最底层外再包一层ChangeNotifierProvider,并把我们需要共享的view_model传给ChangeNotifierProvider的create属性
  runApp(ChangeNotifierProvider(
    create: (ctx) => PersonViewModel(),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ListPage(),
    );
  }
}
  • Model层不需要改动
  • ViewModel层
-----------base_view_model.dart-----------

import 'package:flutter/cupertino.dart';

enum ViewState {
  loading, // 加载中
  success, // 加载成功
  empty, // 加载成功,但数据为空
  failure, // 加载失败
  noNetwork, // 没网
}

class BaseViewModel extends ChangeNotifier {
  ViewState _state = ViewState.success;

  ViewState get state => _state;

  set state(ViewState value) {
    _state = value;
    notifyListeners();
  }
}
-----------person_view_model.dart-----------

/*
 ViewModel的职责:
  1、ViewModel负责获取数据;
  2、ViewModel负责处理数据;
  3、ViewModel负责存储数据。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// Person视图模型
///
/// 【变化一】:要想使用Provider,PersonViewModel得继承自或者混入ChangeNotifier
class PersonViewModel extends BaseViewModel {
  // 命名构造方法,专门用来初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一个_personModel,以便处理数据:ViewModel一对一Model地添加属性并处理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通构造方法
  PersonViewModel();

  /// 存储数据:vm数组
  ///
  /// 真正暴露给外面使用的是vm数组,里面的数据已经处理好了,直接拿着显示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存储数据:错误消息
  String errorMsg = "";

  /// 处理数据:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 处理数据:性别
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 处理数据:年龄
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 请求前缀数据
  Future<void> loadPrefixData(String params) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      //【变化二】
      state = ViewState.success;
    } catch (error) {
      errorMsg = error.toString();

      //【变化二】
      state = ViewState.failure;
    }
  }

  /// 请求后缀数据
  Future<void> loadSuffixData(String params) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      //【变化二】
      state = ViewState.success;
    } catch (error) {
      errorMsg = error.toString();

      //【变化二】
      state = ViewState.failure;
    }
  }
}
  • View层
-----------prefix_list_view_widget.dart-----------

/*
 View的职责:View负责显示数据,那怎么显示数据呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 前缀列表Widget
class PrefixListViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("【PrefixListViewWidget】---build");

    return Container(
      color: Colors.green,
      // 第三步(get的情况):在需要使用共享数据的Widget外包一层Consumer或者Selector来使用共享数据,需要get共享数据的地方使用Consumer
      // Consumer是个泛型,泛的型就是问要使用哪个viewModel里的数据,填进去即可
      child: Consumer<PersonViewModel>(
        // Consumer有一个必添属性builder,接收一个函数作为属性值,该函数的第二个参数就是共享数据存放地
        // 当数据发生变化时,就会触发这个builder函数来刷新Widget,所以我们在这个函数里返回原始的Widget,从而达到Widget包裹Consumer的目的
        builder: (context, personViewModel, child) {
          print("【PrefixListViewWidget】------Consumer");

          return ListView.builder(
            itemCount: personViewModel.prefixPersonViewModelList.length,
            itemBuilder: (context, index) {
              PersonViewModel personVM =
                  personViewModel.prefixPersonViewModelList[index];

              return Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Text(personVM.name),
                  Text(personVM.sex),
                  Text("${personVM.age}"),
                ],
              );
            },
            itemExtent: 100,
          );
        },
      ),
    );
  }
}
-----------suffix_list_view_widget.dart-----------

/*
 View的职责:View负责显示数据,那怎么显示数据呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 后缀列表Widget
class SuffixListViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("【PrefixListViewWidget】---build");

    return Container(
      color: Colors.blue,
      // 第三步(get的情况):在需要使用共享数据的Widget外包一层Consumer或者Selector来使用共享数据,需要get共享数据的地方使用Consumer
      // Consumer是个泛型,泛的型就是问要使用哪个viewModel里的数据,填进去即可
      child: Consumer<PersonViewModel>(
        // Consumer有一个必添属性builder,接收一个函数作为属性值,该函数的第二个参数就是共享数据存放地
        // 当数据发生变化时,就会触发这个builder函数来刷新Widget,所以我们在这个函数里返回原始的Widget,从而达到Widget包裹Consumer的目的
        builder: (context, personViewModel, child) {
          print("【SuffixListViewWidget】------Consumer");

          return ListView.builder(
            itemCount: personViewModel.suffixPersonViewModelList.length,
            itemBuilder: (context, index) {
              PersonViewModel personVM =
                  personViewModel.suffixPersonViewModelList[index];

              return Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Text(personVM.name),
                  Text(personVM.sex),
                  Text("${personVM.age}"),
                ],
              );
            },
            itemExtent: 100,
          );
        },
      ),
    );
  }
}
  • Controller层
-----------list_page.dart-----------

/*
 Controller的职责:
  1、Controller负责持有View,创建View,并把View添加到窗口上显示;
  2、Controller负责持有ViewModel,调用ViewModel的方法去请求数据;
  3、vm --> view:Controller调用vm的方法请求数据,请求完成后vm是通过回调的方式告诉Controller的:
    请求成功后Controller需要调用一下setState来刷一下UI,这样view就会去拿vm里最新存储的数据来展示了
    请求失败后Controller可以toast一下错误信息给用户看,或者调用一下setState来刷一下UI,刷成暂无数据那种view
  4、view --> vm:view产生的变化是通过回调告诉Controller的,Controller可以调用vm的方法把view发生的变化告诉它
 */

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatefulWidget {
  const ListPage({Key? key}) : super(key: key);

  @override
  State<ListPage> createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> {
  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildTwoListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    // 第三步(set的情况):在需要使用共享数据的Widget外包一层Consumer或者Selector来使用共享数据,需要set共享数据的地方使用Selector
    // Selector是个泛型,而且还泛了两个型,第一个泛的型是问要使用哪个viewModel里的数据,填进去即可,第二个泛的型是转换之后的数据类型,比如我这里转换之后依然是使用PersonViewModel,那么我们填一样的类型即可(大多数情况都不需要转吧)
    return Selector<PersonViewModel, PersonViewModel>(
      // Selector有一个必添属性selector,接收一个函数作为属性值,该函数的第二个参数就是共享数据存放地
      // 该函数用来指定Selector的两个泛型之间如何进行数据转换,这里我们不转换,所以返回原先的personViewModel
      // 如果转换后,则下面builder函数真正渲染Widget时的第二个参数就是转换后的ViewModel了,不转换的话builder函数的第二个参数就是原始的ViewModel
      selector: (ctx, personViewModel) {
        return personViewModel;
      },
      // Selector有一个必添属性builder,接收一个函数作为属性值,该函数的第二个参数就是共享数据存放地
      // 当数据发生变化时,就会触发这个builder函数来刷新Widget,所以我们在这个函数里返回原始的Widget,从而达到Widget包裹Selector的目的
      builder: (context, personViewModel, child) {
        print("_buildSearchBarWidget---Selector");

        return Container(
          color: Colors.red,
          child: SearchBarWidget(
            searchTextDidChangeCallback: (text) {
              personViewModel.loadPrefixData(text);
              personViewModel.loadSuffixData(text);
            },
          ),
        );
      },
      // 当共享数据发生变化时,是否执行builder方法重新构建Widget,我们可以设定为 return pre = next,不一样时才刷新,一样就不刷新
      // 但是因为本次案例里是仅仅set共享数据,不需要get变化后的共享数据来刷新Widget的情况,所以这种情况我们总是返回false就ok
      shouldRebuild: (prev, next) {
        return false;
      },
    );
  }

  Widget _buildTwoListViewWidget() {
    return Consumer<PersonViewModel>(
        builder: (context, personViewModel, child) {
      switch (personViewModel.state) {
        case ViewState.success:
          return Column(
            children: [
              Expanded(
                child: _buildPrefixListViewWidget(),
              ),
              Expanded(
                child: _buildSuffixListViewWidget(),
              ),
            ],
          );
        default:
          Fluttertoast.showToast(
            msg: personViewModel.errorMsg,
            gravity: ToastGravity.CENTER,
          );

          return Center(
            child: Text("${personViewModel.errorMsg}"),
          );
      }
    });
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget();
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return SuffixListViewWidget();
  }
}


四、使用GetX的MVVM


GetX支持响应式编程,使用它可以非常简单地实现数据驱动UI的效果,只需要两步:一在ViewModel里把外界想监听的数据通过.obs搞成Observable,二在外界想使用数据的地方通过Obx(() => Widget)包裹真正的Widget搞成Observer,这样就可以数据驱动UI了,这就是它相对于最原始MVVM的第一个变化,这个变化主要解决了ViewModel怎么把变化告诉Controller的问题。

GetX也支持数据共享,也只需要两步:一在Controller里本来创建_viewModel的地方Get.put一下,二完事就可以在任何想使用_viewModel的地方Get.find到它来使用了,这就是它相对于最原始MVVM的第二个变化,这个变化主要解决了viewModel传来传去的问题。

【它的优势就是非常简单地响应式编程数据驱动UI + 非常简单得数据共享方式,比RxDart简单地多。】【它的劣势就是GetX框架的侵入性太强了。】

因此相对于最原始的MVVM来说,你有余力的话可以学学GetX并应用在你的MVVM中;相对于使用Provider的MVVM来说,则强烈推荐使用GetX,它的使用简直太简单了。

当然GetX还提供了很多其它的功能,如相对于系统自带的Navigator更加简单地路由管理App国际化等,可以根据自己的情况选择使用。

  • Model层不需要改动
  • ViewModel层
-----------base_view_model.dart-----------

import 'package:get/get.dart';

enum ViewState {
  loading, // 加载中
  success, // 加载成功
  empty, // 加载成功,但数据为空
  failure, // 加载失败
  noNetwork, // 没网
}

class BaseViewModel extends GetxController {
  Rx<ViewState> state = ViewState.success.obs;
}
-----------person_view_model.dart-----------

/*
 ViewModel的职责:
  1、ViewModel负责获取数据;
  2、ViewModel负责处理数据;
  3、ViewModel负责存储数据。
 */

import 'dart:convert';

import 'package:flutter/services.dart';

import 'package:flutter_mvvm/classes/model/person_model.dart';
import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';

/// Person视图模型
class PersonViewModel extends BaseViewModel {
  // 命名构造方法,专门用来初始化_personModel
  PersonViewModel._from(PersonModel? personModel) {
    _personModel = personModel;
  }

  // 持有一个_personModel,以便处理数据:ViewModel一对一Model地添加属性并处理,搞成getter方法即可
  PersonModel? _personModel;

  /// 普通构造方法
  PersonViewModel();

  /// 存储数据:vm数组
  ///
  /// 真正暴露给外面使用的是vm数组,里面的数据已经处理好了,直接拿着显示就行了
  List<PersonViewModel> prefixPersonViewModelList = [];
  List<PersonViewModel> suffixPersonViewModelList = [];

  /// 存储数据:错误消息
  String errorMsg = "";

  /// 处理数据:姓名
  String get name {
    return _personModel?.name ?? "";
  }

  /// 处理数据:性别
  ///
  /// 0-未知,1-男,2-女
  String get sex {
    if (_personModel?.sex == 1) {
      return "男";
    } else if (_personModel?.sex == 2) {
      return "女";
    } else {
      return "未知";
    }
  }

  /// 处理数据:年龄
  int get age {
    return _personModel?.age ?? 0;
  }

  /// 请求前缀数据
  Future<void> loadPrefixData(String params) async {
    await Future.delayed(Duration(seconds: 1));

    try {
      String path = "lib/assets/json/${params}_prefix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      prefixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        prefixPersonViewModelList.add(personViewModel);
      }

      loadSuffixData(params);
    } catch (error) {
      errorMsg = error.toString();

      state.value = ViewState.failure;
    }
  }

  /// 请求后缀数据
  Future<void> loadSuffixData(String params) async {
    await Future.delayed(Duration(seconds: 2));

    try {
      String path = "lib/assets/json/${params}_suffix.json";

      String jsonString = await rootBundle.loadString(path);
      List list = jsonDecode(jsonString);

      suffixPersonViewModelList.clear();
      for (Map<String, dynamic> map in list) {
        PersonModel personModel = PersonModel.fromJson(map);
        PersonViewModel personViewModel = PersonViewModel._from(personModel);
        suffixPersonViewModelList.add(personViewModel);
      }

      state.value = ViewState.success;
    } catch (error) {
      errorMsg = error.toString();

      state.value = ViewState.failure;
    }
  }
}
  • View层
-----------prefix_list_view_widget.dart-----------

/*
 View的职责:View负责显示数据,那怎么显示数据呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 前缀列表Widget
class PrefixListViewWidget extends StatelessWidget {
  final _personViewModel = Get.find<PersonViewModel>();

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: Obx(
        () {
          print("---PrefixListViewWidget---");

          if (_personViewModel.state.value == ViewState.failure) {
            print(_personViewModel.errorMsg);

            return Center(
              child: Text("${_personViewModel.errorMsg})"),
            );
          } else {
            return ListView.builder(
              itemCount: _personViewModel.prefixPersonViewModelList.length,
              itemBuilder: (context, index) {
                PersonViewModel personVM =
                _personViewModel.prefixPersonViewModelList[index];

                return Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Text(personVM.name),
                    Text(personVM.sex),
                    Text("${personVM.age}"),
                  ],
                );
              },
              itemExtent: 100,
            );
          }
        },
      ),
    );
  }
}
-----------suffix_list_view_widget.dart-----------

/*
 View的职责:View负责显示数据,那怎么显示数据呢?View可以持有ViewModel。
 */

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'package:flutter_mvvm/classes/view_model/base_view_model.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 后缀列表Widget
class SuffixListViewWidget extends StatelessWidget {
  final _personViewModel = Get.find<PersonViewModel>();

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: Obx(() {
        print("---SuffixListViewWidget---");

        if (_personViewModel.state.value == ViewState.failure) {
          print(_personViewModel.errorMsg);

          return Center(
            child: Text("${_personViewModel.errorMsg})"),
          );
        } else {
          return ListView.builder(
            itemCount: _personViewModel.suffixPersonViewModelList.length,
            itemBuilder: (context, index) {
              PersonViewModel personVM =
                  _personViewModel.suffixPersonViewModelList[index];

              return Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Text(personVM.name),
                  Text(personVM.sex),
                  Text("${personVM.age}"),
                ],
              );
            },
            itemExtent: 100,
          );
        }
      }),
    );
  }
}
  • Controller层
-----------list_page.dart-----------

/*
 Controller的职责:
  1、Controller负责持有View,创建View,并把View添加到窗口上显示;
  2、Controller负责持有ViewModel,调用ViewModel的方法去请求数据;
  3、vm --> view:Controller调用vm的方法请求数据,请求完成后vm是通过回调的方式告诉Controller的:
    请求成功后Controller需要调用一下setState来刷一下UI,这样view就会去拿vm里最新存储的数据来展示了
    请求失败后Controller可以toast一下错误信息给用户看,或者调用一下setState来刷一下UI,刷成暂无数据那种view
  4、view --> vm:view产生的变化是通过回调告诉Controller的,Controller可以调用vm的方法把view发生的变化告诉它
 */

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'package:flutter_mvvm/classes/widget/search_bar_widget.dart';
import 'package:flutter_mvvm/classes/widget/prefix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/widget/suffix_list_view_widget.dart';
import 'package:flutter_mvvm/classes/view_model/person_view_model.dart';

/// 列表界面
class ListPage extends StatelessWidget {
  final _personViewModel = Get.put(PersonViewModel());

  @override
  Widget build(BuildContext context) {
    print("build");

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter MVVM"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            _buildSearchBarWidget(),
            Expanded(
              child: _buildPrefixListViewWidget(),
            ),
            Expanded(
              child: _buildSuffixListViewWidget(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSearchBarWidget() {
    print("_buildSearchBarWidget");

    return Container(
      color: Colors.red,
      child: SearchBarWidget(
        searchTextDidChangeCallback: (text) {
          _personViewModel.loadPrefixData(text);
        },
      ),
    );
  }

  Widget _buildPrefixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return PrefixListViewWidget();
  }

  Widget _buildSuffixListViewWidget() {
    print("_buildPrefixListViewWidget");

    return SuffixListViewWidget();
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容