Flutter - fast_mvvm 作者详解

fast_mvvm 创作思路

基于原生Android开发基础,在初次学习Flutter的时候就开始查找关于MVVM框架的开源,但苦于Flutter正在成长中,没有一个很完整的整体框架,所以借鉴了网上部分思路最终开源了 fast_mvvm

快速开发,自由配置,各种状态页,刷新颗粒度控制,兼容下拉刷新上拉加载,电匠工坊作者线上项目使用中, 安卓和苹果都已上架,放心使用。

有问题可以加我 QQ 275916180,欢迎来讨论。
项目在 GitHub上开源了,望大家点赞谢谢。

Pub https://pub.flutter-io.cn/packages/fast_router
GitHub https://github.com/StrangerKjq/fast_router
fast_router 修改自 fluro 的新版本 加入一些新方法
日常使用,加入一些便捷方法,可以抛弃Navigator了
支持ios 左滑 跟 android原生路由跳转

Pub https://pub.flutter-io.cn/packages/fast_develop
GitHub https://github.com/StrangerKjq/fast_develop
fast_develop 快速开发,支持配置,接入屏幕适配、数据安全转换、oss上传、快捷样式设置、状态栏、存储、多级联动、图片显示\选择\裁剪、dio封装、下拉刷新等常用组件。

Demo 讲解

这里模拟了文章列表。
Model:UserModel
BaseListViewModel: ArticleVM
View:ArticlePage
数据实体类 BaseEntity:ArticleEntity
主要讲解了初始化配置, 状态页效果,根布局刷新,数据获取。

UserModel

首先创建项目模块所需要用的Model
这里创建UserModel

class UserModel extends BaseModel {
  /// 登录
  Future<bool> login(String account, String psd) async {
    await Future.delayed(Duration(seconds: 3));
    return true;
  }

  /// 资讯列表
  Future<DataResponse<ArticleEntity>> getArticleList() async {
    await Future.delayed(Duration(seconds: 2));
    
    var entity = ArticleEntity([
      ArticleItem("1", "好的", "内容内容内容内容内容", DateTime.now().toString()),
      ArticleItem("1", "好的", "内容内容内容内容内容", DateTime.now().toString()),
    ]);
    
    DataResponse dataResponse =
        DataResponse<ArticleEntity>(entity: entity, totalPageNum: 3);
    return dataResponse;
  }
}

初始化

在APP首页启动的时候初始化框架。
调用initMVVM(),装载UserModel,配置上拉加载下拉刷新;
选择文章页面是否根布局刷新选项,是否配置单独的状态页。


class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  void initState() {
    initMVVM<BaseViewModel>(
      [UserModel()],
      controllerBuild: () => EasyRefreshController(),
      resetRefreshState: (c) =>
          (c as EasyRefreshController)?.resetRefreshState(),
      finishRefresh: (c, {bool success, bool noMore}) =>
          (c as EasyRefreshController)
              ?.finishRefresh(success: success, noMore: noMore),
      resetLoadState: (c) => (c as EasyRefreshController)?.resetLoadState(),
      finishLoad: (c, {bool success, bool noMore}) =>
          (c as EasyRefreshController)
              ?.finishLoad(success: success, noMore: noMore),
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: SelectPage(),
    );
  }
}

class SelectVM extends BaseViewModel {
  ValueNotifier<bool> isLoadData = ValueNotifier(true);
  ValueNotifier<bool> isConfigState = ValueNotifier(false);
}

class SelectPage extends StatelessWidget with BaseView<SelectVM> {
  @override
  ViewConfig<SelectVM> initConfig(BuildContext context) =>
      ViewConfig(vm: SelectVM());

  @override
  Widget vmBuild(
      BuildContext context, SelectVM vm, Widget child, Widget state) {
    return Scaffold(
      appBar: AppBar(title: Text("选择")),
      body: ListView(
        children: <Widget>[
          ListTile(
            title: Text("是否加载数据,用来测试状态页和重新加载数据"),
            trailing: ValueListenableBuilder(
              valueListenable: vm.isLoadData,
              builder: (_, value, __) => Switch(
                value: value,
                onChanged: (value) => vm.isLoadData.value = value,
              ),
            ),
          ),
          ListTile(
            title: Text("是否单独配置状态页,用来测试状态页和重新加载数据"),
            trailing: ValueListenableBuilder(
              valueListenable: vm.isConfigState,
              builder: (_, value, __) => Switch(
                value: value,
                onChanged: (value) => vm.isConfigState.value = value,
              ),
            ),
          ),
          ListTile(
            title: Text("根布局刷新"),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => ArticlePage(
                    true,
                    configState: vm.isConfigState.value,
                    loadData: vm.isLoadData.value,
                  ),
                ),
              );
            },
          ),
          ListTile(
            title: Text("根布局不刷新"),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => ArticlePage(
                    false,
                    configState: vm.isConfigState.value,
                    loadData: vm.isLoadData.value,
                  ),
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

ArticleEntity

模拟接口返回的数据实体类:

class ArticleEntity extends BaseEntity {
  List<ArticleItem> list;

  ArticleEntity(this.list);
}

class ArticleItem {
  String id;
  String title;
  String content;
  String time;

  ArticleItem(this.id, this.title, this.content, this.time);
}

ArticleVM

UserModel 继承 BaseModel,ArticleEntity继承BaseEntity,ArticleItem 暂没要求。
是否首次加载数据测试空状态页的效果
创建vnTime 用来监听List第一个ArticleItem的时间刷新


class ArticleVM
    extends BaseListViewModel<UserModel, ArticleEntity, ArticleItem> {
  ArticleVM(this.isLoadData);
  bool isLoadData = true;

  /// 首次加载
  bool firstLoad = true;
  ValueNotifier<String> vnTime = ValueNotifier("暂无");

  @override
  void jointList(ArticleEntity newEntity) => entity.list.addAll(newEntity.list);

  @override
  List<ArticleItem> get list => entity.list;
  @override
  Future<DataResponse<ArticleEntity>> requestHttp(
      {bool isLoad, int page, params}) {
    /// 判断是否加载数据, 测试状态页用
    if (!isLoadData && firstLoad) {
      firstLoad = false;
      return null;
    }
    return model.getArticleList();
  }

  @override
  void initResultData() {
    vnTime.value = list[0].time;
  }

  /// 修改第一个数据的时间
  void modifyFistTime() {
    list[0].time = DateTime.now().toString();
    vnTime.value = list[0].time;
    notifyListeners();
  }
}

ArticlePage View

文章具体页面,显示一个列表,下方显示第一个item对应的时间,和根布局刷新的时间。

class ArticlePage extends StatelessWidget with BaseView<ArticleVM> {
  const ArticlePage(
    this.rootRefresh, {
    Key key,
    this.configState = false,
    this.loadData = true,
  }) : super(key: key);

  /// 是否全局刷新
  final bool rootRefresh;
  final bool configState;
  final bool loadData;

  @override
  ViewConfig<ArticleVM> initConfig(BuildContext context) {
    var _empty = configState ? (vm) => Center(child: Text("单独配置:empty")) : null;
    return rootRefresh
        ? ViewConfig<ArticleVM>(vm: ArticleVM(loadData), empty: _empty)
        : ViewConfig<ArticleVM>.noRoot(vm: ArticleVM(loadData), empty: _empty);
  }

  @override
  Widget vmBuild(
      BuildContext context, ArticleVM vm, Widget child, Widget state) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: Text("文章")),
      bottomNavigationBar: state != null
          ? SizedBox()
          : Container(
              color: Colors.amber,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  MaterialButton(
                    onPressed: vm.modifyFistTime,
                    color: Colors.white,
                    child: Text("修改第一个Item时间,测试全局刷新"),
                  ),
                  ValueListenableBuilder<String>(
                    valueListenable: vm.vnTime,
                    builder: (_, value, __) {
                      return Text("第一个Item时间:$value");
                    },
                  ),
                  Text("根布局刷新时间:${DateTime.now().toString()}"),
                ],
              ),
            ),
      body: state ??
          EasyRefresh(
            controller: vm.refreshController,
            onLoad: vm.loadMore,
            onRefresh: vm.pullRefresh,
            child: ListView.builder(
              itemCount: vm.list.length,
              itemBuilder: (ctx, index) {
                return Selector<ArticleVM, ArticleItem>(
                  selector: (_, aVM) => aVM.list[index],
                  shouldRebuild: (pre, next) => pre == next,
                  builder: (_, ArticleItem value, __) => _item(value),
                );
              },
            ),
          ),
    );
  }

  Widget _item(ArticleItem item) {
    return Container(
      color: Colors.lightGreen,
      margin: EdgeInsets.all(8),
      padding: EdgeInsets.all(4),
      child: Column(
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Text(item.title),
              Text(item.time),
            ],
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(item.content),
          ),
        ],
      ),
    );
  }
}

数据刷新

通过ValueListenableBuilder 监听在ArticleVM创建的ValueNotifier 对象,实现局部刷新。
通过Selector 监听ArticleEntityListArticleItem 的变化

ValueListenableBuilder<String>(
                  valueListenable: vm.vnTime,
                  builder: (_, value, __) {
                    return Text("第一个Item时间:$value");
                  },
                )
Selector<ArticleVM, ArticleItem>(
                  selector: (_, aVM) => aVM.list[index],
                  shouldRebuild: (pre, next) => pre == next,
                  builder: (_, ArticleItem value, __) => _item(value),
                )

源码介绍

做开发大家或多或少都听说过MVC,而 MVPMVVM都是基于MVC进化而来的产物,为了更好的拆分业务与界面,提高整体项目结构,便于后期维护与单元测试。关于具体的区别水平有限这些就不细讲了。

Entity 实体类

Entity 实体类主要负责将JSON数据进行格式转换。
业务实现时继承 BaseEntity 暂时没有处理,后期开发迭代会用到。

class ArticleEntity extends BaseEntity {
  List<ArticleItem> list;

  ArticleEntity(this.list);
}

class ArticleItem {
  String id;
  String title;
  String content;
  String time;

  ArticleItem(this.id, this.title, this.content, this.time);
}

Model 数据服务层

Model 数据服务层,跟其他类MVC模式一样,不管最终对接的是数据库还是网络API或是其它,都是在负责数据的存储,并提供访问数据的接口,以支持数据的增删改查的基本操作。
源码介绍:

/// 基类的API 声明API
mixin BaseRepo {}

/// 基类Model  具体实现API
class BaseModel with BaseRepo {}

假设现在有一个用户模块UserModel继承BaseModel负责用户的所有接口。
具体业务实现:

class UserModel extends BaseModel {
  /// 登录
  Future<bool> login(String account, String psd) async {
    await Future.delayed(Duration(seconds: 3));
    return true;
  }
  
  /// 资讯列表
  Future<DataResponse<ArticleEntity>> getArticleList() async {
    await Future.delayed(Duration(seconds: 2));

    var entity = ArticleEntity(
        [ArticleItem("1", "好的", "内容内容内容内容内容", DateTime.now().toString())]);

    DataResponse dataResponse =
        DataResponse<ArticleEntity>(entity: entity, totalPageNum: 3);
    return dataResponse;
  }
}

接口数据处理封装

response接口未格式化数据。

class DataResponse<T> {
  T entity;
  bool result;
  Response response;
  int totalPageNum;
  get data => response.data;

  DataResponse({
    @required this.entity,
    this.result = false,
    this.response,
    this.totalPageNum = 1,
  });
}

ViewModel 数据与页面的绑定调用层

ViewModel 主要负责调用Model层获取数据,将数据和方法提供给View层来调用,因暂时没有找到获取View层控件的方法该功能暂没有实现。

数据刷新基于官方的provider来实现。

BaseViewModel

源代码可以看到 这里继承了ChangeNotifier,并且需要范型BaseModelBaseEntity

/// 基类 VM
abstract class BaseViewModel<M extends BaseModel, E extends BaseEntity>
	extends ChangeNotifier 

页面状态管理枚举类ViewModelState 提供 idle 闲置 busy 加载中 empty 空数据 error 错误 unAuthorized 账号未授权

/// ViewModel的状态 控制页面基础显示
enum ViewModelState { idle, busy, empty, error, unAuthorized }

初始化ViewModel
defaultOfParams 在请求数据用的参数下面会讲到。
viewState 指定状态,默认为闲置。

自动填充BaseModel

 /// 根据状态构造
  /// 子类可以在构造函数指定需要的页面状态
  /// FooModel():super(viewState:ViewState.busy);
  BaseViewModel({ViewModelState viewState, this.defaultOfParams})
      : _viewState = viewState ?? ViewModelState.idle {
    init(false);
    Future.delayed(Duration(seconds: 1), () => init(true));
  }

  /// model
  M model;

  M getModel() => null;

  @mustCallSuper
  void init(bool await) {
    if (!await) {
      model = getModel() ?? getModelGlobal<M>();
//      if (isSaveVM()) _addVM(this);
    } else {
      _eventButAddInit(portMap);
      _disposeInit();
    }
  }

初始化 EventBus 事件通知 批量端口注册 fast_event_bus fast_event_bus
子类实现 portMap
案例:

 @override
  Map<String, EventListen> get portMap => {
        Constant.invoice_select: invoiceSelect,
        Constant.address_select_send: addressSelect,
      };

源码:


  /// 端口 key 跟 回调监听
  Map<String, EventListen> get portMap => Map<String, EventListen>();

  /// 绑定端口跟回调
  void _eventButBindListen(String key, EventListen listen) {
    EventBus.getDefault().register(key, listen);
  }

  /// 绑定初始化 大量绑定
  void _eventButAddInit(Map<String, EventListen> portMap) {
    portMap?.forEach((key, callback) {
      _eventButBindListen(key, callback);
    });
  }

  /// 端口删除
  void eventButDelete(String key) {
    EventBus.getDefault().unregister(key);
  }

  /// 端口添加
  @mustCallSuper
  bool eventButAdd(String key, EventListen listen) {
    portMap.update(key, (l) => listen, ifAbsent: () => listen);
    return EventBus.getDefault().register(key, listen);
  }

初始化 dispose注册在页面销毁的时候销毁内存占用
子类实现方法 waitDispose()
案例:

  TextEditingController name = TextEditingController();
  TextEditingController phone = TextEditingController();
  TextEditingController area = TextEditingController();
  TextEditingController tecAddress = TextEditingController();
  @override
  List waitDispose() => [name, phone, area, tecAddress];

源码:

  List _disposeWait = [];

  void _disposeInit() {
    for (var item in waitDispose()) _disposeAdd(item);
  }

  void _disposeAdd(item) {
    if (item.dispose != null) _disposeWait.add(item);
  }

  /// 清理内存占用
  void _disposeList() {
    for (var item in _disposeWait)
      if (item != null) {
        try {
          if (item is StreamSubscription) {
            item.cancel();
          } else {
            item.dispose();
          }
        } catch (e, s) {
          handleCatch(e, s);
        } finally {
          item = null;
        }
      }
  }

  @override
  void dispose() {
    _disposed = true;
    for (var key in portMap.keys) {
      eventButDelete(key);
    }
    _disposeList();
    super.dispose();
  }

接下来看关于状态的设置:

 /// 当前的页面状态,默认为busy,可在viewModel的构造方法中指定;
  ViewModelState _viewState;

  ViewModelState get viewState => _viewState;

  /// 出错时的message
  String _errorMessage;

  String get errorMessage => _errorMessage;

  /// 以下变量是为了代码书写方便,加入的变量.严格意义上讲,并不严谨
  bool get busy => viewState == ViewModelState.busy;

  bool get idle => viewState == ViewModelState.idle;

  bool get empty => viewState == ViewModelState.empty;

  bool get error => viewState == ViewModelState.error;

  bool get unAuthorized => viewState == ViewModelState.unAuthorized;

  void setBusy(bool value) {
    _errorMessage = null;
    viewState = value ? ViewModelState.busy : ViewModelState.idle;
  }

  void setEmpty() {
    _errorMessage = null;
    viewState = ViewModelState.empty;
  }

  void setError(String message) {
    _errorMessage = message;
    viewState = ViewModelState.error;
  }

  void setUnAuthorized() {
    _errorMessage = null;
    viewState = ViewModelState.unAuthorized;
  }
  
  /// 最终修改状态并通知页面刷新
  set viewState(ViewModelState viewState) {
    _viewState = viewState;
    notifyListeners();
  }

下面讲解页面数据请求
是否是http请求,还是本地数据装载。
在获取到数据后判断空值。
然后执行initResultData()对数据进行下一步自定义处理。

bool isHttp() => true;

  /// 进入页面isInit loading
  Future<void> viewRefresh({
    bool showLoad = false,
    dynamic params,
    bool notifier = true,
    bool busy = true,
  }) async {
    if (busy && !showLoad) setBusy(true);
    bool result = false;
    result = await _request(param: params);
    _notifyIntercept = !notifier;
//    LogUtil.printLog("notifier : $notifier _notifyIntercept:$_notifyIntercept");
    if (!result) {
      setEmpty();
    } else {
      ///改变页面状态为非加载中
      setBusy(false);
    }
  }

  /// 请求数据
  Future<bool> _request({param}) async {
    try {
      var data = await _httpOrData(false, BaseListViewModel.pageFirst, param);
      if (data == null || data.entity == null) {
        return false;
      } else {
        entity = data.entity;
        initResultData();
        return true;
      }
    } catch (e, s) {
      handleCatch(e, s);
      return false;
    }
  }

  /// 判断http或者data
  Future<DataResponse<E>> _httpOrData(bool isLoad, int page, param) async {
    return isHttp()
        ? await requestHttp(
            isLoad: isLoad, page: page, params: param ?? defaultOfParams)
        : await requestData(isLoad, page);
  }

本地数据装载
异步方法 并返回成功和失败

  /// 非http请求
  Future<DataResponse<E>> requestData(bool isLoad, int page) async => null;

http接口请求
源码:

  /// http请求
  Future<DataResponse<E>> requestHttp(
          {@required bool isLoad, int page, params}) async =>
      null;

在请求时调用model提供的API来获取数据。案例:

  @override
  Future<DataResponse<ArticleEntity>> request({bool isLoad, int page, params}) {
    return model.getArticleList();
  }

页面销毁
销毁初始化注册的事件端口和监听Steam等对象。防止内存泄露

  @override
  void dispose() {
    _disposed = true;
    for (var key in portMap.keys) {
      eventButDelete(key);
    }
    _disposeList();
    super.dispose();
  }

BaseListViewModel

BaseViewModel上进一步封装为长列表页面提供下拉刷新和上拉加载,额外需要一个List中Item类 可以是dynamic

/// 基类 ListVM
abstract class BaseListViewModel<M extends BaseModel, E extends BaseEntity, I>
    extends BaseViewModel<M, E>

初始化参数
页面配置显示当前页码

 BaseListViewModel({params}) : super(defaultOfParams: params);

  /// 分页第一页页码
  static int pageNumFirst = 1;

  /// 当前页码
  int _currentPageNum = pageNumFirst;
  static int _totalPageNum = 1;

  /// 跟EasyRefresh 相关配置
  EasyRefreshController _refreshController = EasyRefreshController();
  EasyRefreshController get refreshController => _refreshController;

提供一个List对外方法:

  @protected
  List<I> get list;

案例:

EasyRefresh(
            controller: vm.refreshController,
            onLoad: vm.loadMore,
            onRefresh: vm.viewRefresh,
            child: ListView.builder(
                itemCount: vm.list.length,
                itemBuilder: (ctx, index) => _item(vm.list[index])),
          )

下拉刷新:

/// 下拉刷新
  Future<bool> httpRequest({param}) async {
    try {
      _currentPageNum = pageNumFirst;
      DataResponse<E> data = await request(
          isLoad: false, page: pageNumFirst, params: param ?? defaultOfParams);
      refreshController.finishRefresh();
      refreshController.resetLoadState();
      if (_checkData(false, data)) {
        return false;
      } else {
        initResultData();
        _totalPageNum = data.totalPageNum ?? 1;
        refreshController.finishLoad(success: true);
        return true;
      }
    } catch (e, s) {
      refreshController.finishRefresh(success: false);
      refreshController.resetLoadState();
      handleCatch(e, s);
      return false;
    }
  }

上拉加载:

  /// 上拉加载更多
  Future<void> loadMore() async {
//    print('------> current: $_currentPageNum  total: $_totalPageNum');
    if (_currentPageNum >= _totalPageNum) {
      refreshController.finishLoad(success: true, noMore: true);
    } else {
      var cPage = ++_currentPageNum;
      //debugPrint('ViewStateRefreshListViewModel.loadMore page: $currentPage');
      try {
        var data =
            await request(isLoad: true, page: cPage, params: defaultOfParams);
        if (_checkData(true, data)) {
          _currentPageNum--;
          refreshController.finishLoad(success: true, noMore: true);
        } else {
          if (_currentPageNum >= _totalPageNum) {
            refreshController.finishLoad(success: true, noMore: true);
          } else {
            refreshController.finishLoad(success: true, noMore: false);
            refreshController.resetLoadState();
          }
          notifyListeners();
        }
      } catch (e, s) {
        _currentPageNum--;
        refreshController.finishLoad(success: false);
        refreshController.resetLoadState();
        debugPrint('error--->\n' + e.toString());
        debugPrint('stack--->\n' + s.toString());
      }
    }
  }

对数据判断提供的新的方法,并提供空数组子类的自己实现。

  /// 验证数据是否为空
  bool _checkData(bool isLoad, DataResponse<E> data) {
    if (data == null || data.entity == null) return true;
    if (isLoad) {
      jointList(data.entity);
    } else {
      entity = data.entity;
    }
    return judgeNull(data);
  }
  
  /// 判断数组是否为空
  @protected
  bool judgeNull(DataResponse<E> data) => list == null || list.isEmpty;

数组拼接:

  void jointList(E newModel);

案例:
当获取数据后对数组重装,因数据List的不确定性,这里由子类实现。

  @override
  void jointList(ArticleEntity newEntity) => entity.list.addAll(newEntity.list);

View 页面层

View 主要负责页面的呈现展示。
Flutter 主要有StatelessWidgetStatefulWidget两种控件用来展示页面,BaseVIew的设计思路采用的扩展,对StatelessWidgetStatefulWidget进行扩展使用,不破坏控件完整性。

ViewConfig

ViewConfig 页面配置类主要用于配置页面属性。
根布局刷新方式,空数据验证,是否初始加载,页面状态页自定义等。
源码:

/// view层 配置用类
class ViewConfig<VM extends BaseViewModel> {
  ViewConfig({
    @required this.vm,
    this.child,
    this.color,
    this.load = true,
    this.checkEmpty = true,
    this.state,
    this.value = false,
    this.busy,
    this.empty,
    this.error,
    this.unAuthorized,
  })  : this.root = true,
        this._firstLoad = true;

  ViewConfig.value({
    @required this.vm,
    this.child,
    this.color,
    this.load = false,
    this.checkEmpty = true,
    this.state,
    this.value = true,
    this.busy,
    this.empty,
    this.error,
    this.unAuthorized,
  })  : this.root = true,
        this._firstLoad = true;

  ViewConfig.noRoot({
    @required this.vm,
    this.child,
    this.color,
    this.load = true,
    this.checkEmpty = true,
    this.state,
    this.value = false,
    this.busy,
    this.empty,
    this.error,
    this.unAuthorized,
  })  : this.root = false,
        this._firstLoad = true;

  /// VM
  VM vm;

  Widget child;
  VSBuilder<VM> busy;
  VSBuilder<VM> empty;
  VSBuilder<VM> error;
  VSBuilder<VM> unAuthorized;

  /// 背景颜色
  Color color;

  /// 加载
  bool load;

  /// 是否根布局刷新 采用 [Selector]
  bool root;

  /// 首次加载
  bool _firstLoad;

  /// [ChangeNotifierProvider.value] 或者[ChangeNotifierProvider]
  bool value;

  /// 是否验证空数据
  bool checkEmpty;

  /// 页面变化控制
  int state;
}

提供了三种构造方法。
默认构造必须传递ViewModel并且不能为空,首次创建,不能被监听。

ChangeNotifierProvider<T>(
        create: (_) => changeNotifier.vm, child: child)

value构造 已经被创建,有监听存在,ViewModel可以为空。
可以用Providr.of(context)获取到ViewModel对象。

    changeNotifier.vm = Provider.of<T>(context);
    return ChangeNotifierProvider<T>.value(
        value: changeNotifier.vm, child: child)

noRoot 构造 对根布局刷新进行优化,控制当前页面的根布局刷新不会因为ViewModel调用了notifyListeners()方法而全局刷新。

状态页讲解

案例:
全局配置状态, 这里的vm是BaseViewModel

initMVVM(
      [UserModel(), ShopModel()],
      empty: (vm) => VSEmptyWidget(onTap: (_) => vm.viewRefresh()),
      error: (vm) => VSWidget(onTap: (_) => vm.viewRefresh()),
      unAuthorized: (vm) => VSUnAuthWidget(onTap: (_) => vm.viewRefresh())
    );

页面单独配置状态,vm是对应页面的BaseViewModel的子类,比如文章对应的ArticleVM

 @override
  ViewConfig<ArticleVM> initConfig(BuildContext context) {
    return rootRefresh
        ? ViewConfig<ArticleVM>(vm: ArticleVM())
        : ViewConfig<ArticleVM>.noRoot(vm: ArticleVM(), busy: (vm) => Container());
  }

页面根布局处理

通过ViewConfig的配置,来进行根布局刷新管理,
默认情况根布局会响应每次的刷新 if (config.root) return true;
否则就只响应初始数据加载时的变化,后期页面刷新都会忽略进而优化页面渲染。
源码:

/// root 根节点加工 根节点是否需要刷新,不刷新就执行一次刷新 更新第一次状态变化
Widget _root<VM extends BaseViewModel>(
    BuildContext context, ViewConfig config, VMBuilder builder) {
  /// 是否根节点需要刷新
  return _availableCNP<VM>(
    context,
    config,
    child: Selector<VM, dynamic>(
      child: config.child,
      selector: (ctx, vm) => vm.entity,
      shouldRebuild: (_, __) {
        if (config.root) return true;
        if (!config._firstLoad) return false;
        config._firstLoad = false;
        return true;
      },
      builder: (ctx, value, child) =>
          _viewState(config, (state) => builder(ctx, config.vm, child, state)),
    ),
  );
}

页面状态管理

这一步东西较多,主要负责页面的状态和后期跨页面刷新的逻辑。
首先判断页面状态ViewConfig配置,配置为空则采用默认状态页。
然后处理页面背景。
最后判断是否需要跨页面刷新 是否配置ViewConfig.state 提前埋点。
源码:

/// 页面状态展示 空 正常 错误 忙碌
Widget _viewState<VM extends BaseViewModel>(
    ViewConfig data, Widget Function(Widget state) builder) {
  VM viewModel = data.vm;
  var bgColor = data.color;
  var checkEmpty = data.checkEmpty;
  var state = data.state;
  var empty = data.empty == null ? null : data.empty(viewModel);
  var busy = data.busy == null ? null : data.busy(viewModel);
  var error = data.error == null ? null : data.error(viewModel);
  var un = data.unAuthorized == null ? null : data.unAuthorized(viewModel);

  Widget stateView;
  if (viewModel == null || checkEmpty && viewModel.empty) {
    stateView = empty ??
        Container(
          color: bgColor,
          child: ViewStateEmptyWidget(onTap: () => viewModel.viewRefresh()),
        );
  } else if (viewModel.busy) {
    stateView = busy ?? ViewStateBusyWidget(backgroundColor: bgColor);
  } else if (viewModel.error) {
    stateView = error ?? ViewStateWidget(onTap: () => viewModel.viewRefresh());
  } else if (viewModel.unAuthorized) {
    stateView =
        un ?? ViewStateUnAuthWidget(onTap: () => viewModel.viewRefresh());
  }

  Widget view = builder(stateView);
  if (bgColor != null) {
    view = Container(child: view, color: bgColor);
  }

  if (state == null) {
    return view;
  } else {
    /// view状态变化提醒
    var changer = ValueListenableBuilder(
      valueListenable: changerStateGet(state).vn,
      builder: (_, changer, __) {
//          LogUtil.printLog("state : ${state.toString()} value: $changer");
        try {
          var vsChanger = changerStateCheck(state);
          if (vsChanger.changer) {
//                  LogUtil.printLog("state : ${state.toString()} value: $changer"
//                      "notifier: ${vsChanger.notifier}");
            viewModel.viewRefresh(notifier: vsChanger.notifier, busy: false);
          }
        } catch (e) {
          print(e);
        }
        return SizedBox();
      },
    );
    return Stack(children: <Widget>[changer, Positioned.fill(child: view)]);
  }
}

跨页面刷新

获取状态配置,验证是否已经刷新,更新页面
这里就是上面讲到了状态的埋点,然后其他页面处理时需要刷新则调用changerStateUpdate
举例:商城购物车,在商品详情点击添加购物车后,需要刷新购物车列表。
源码:

class _ViewStateNotifier {
  ValueNotifier<bool> vn;
  bool notifier;
  _ViewStateNotifier(this.vn, {this.notifier = true});
}

/// 状态通知 跨页面通知数据需要变动
class ViewStateNotifier {
  bool changer;
  bool notifier;

  ViewStateNotifier(this.changer, this.notifier);
}

/// 全局状态变动存储
Map<int, _ViewStateNotifier> _changerState = {};

/// 获取状态配置
_ViewStateNotifier changerStateGet(int state) {
  if (!_changerState.containsKey(state)) {
    _changerState[state] = _ViewStateNotifier(ValueNotifier(false));
  }
  return _changerState[state];
}

/// 更新页面状态
void changerStateUpdate(int state, {bool notifier = true}) {
  if (_changerState.containsKey(state)) {
    _changerState[state].vn.value = true;
    _changerState[state].notifier = notifier;
  }
}

/// 验证是否需要变化
ViewStateNotifier changerStateCheck(int state) {
  var result = _changerState[state].vn.value;
  _changerState[state].vn.value = false;
  return ViewStateNotifier(result, _changerState[state].notifier);
}

BaseView

BaseView 是基于StatelessWidget实现,需要继承BaseViewModel范型。
子类必须实现initConfigvmBuild,并且删除build的实现。
initConfig 提供页面的初始化配置。
vmBuild 页面最终呈现调用的方法。
源码介绍:

/// 基类 view 扩展[StatelessWidget]
mixin BaseView<VM extends BaseViewModel> on StatelessWidget {
  /// 初始化配置
  @protected
  ViewConfig<VM> initConfig(BuildContext context);

  /// VM 相关
  @protected
  Widget vmBuild(BuildContext context, VM vm, Widget child, Widget state);

  /// 初始化操作 加载等
  _init(BuildContext context, ViewConfig<VM> config) async {
    config.vm.context ??= context;
    if (config.load) await config.vm.viewRefresh();
  }

  /// 不要使用  推荐使用 [vmBuild]
  @override
  @deprecated
  Widget build(BuildContext ctx) {
//    LogUtil.printLog("build:----" + this.runtimeType.toString());
    var config = initConfig(ctx);
    if (config == null) throw "initConfig 方法 返回空值";

    /// 是否需要加载
    if (!config.load) return _root<VM>(ctx, config, vmBuild);

    return FutureBuilder(
        future: _init(ctx, config),
        builder: (ctx, __) => _root<VM>(ctx, config, vmBuild));
  }
}

vmBuild 详解
子类必须实现的方法,VM就是当前页面所需要的BaseViewModelchild 就是当前页面不会有状态变化的控件,state就是当前页面状态切换提供给子类展示和判断,只有当页面加载顺利完成才会为空。
源码:

  /// VM 相关
  @protected
  Widget vmBuild(BuildContext context, VM vm, Widget child, Widget state);

案例:
文章页面可以看到,我返回了Scaffoldbody里面我判断了state是否为空,当状态为空时,代表数据加载全部完成然后返回一个列表来展示数据。

  @override
  Widget vmBuild(
      BuildContext context, ArticleVM vm, Widget child, Widget state) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: Text("文章")),
      body: state ??
          EasyRefresh(
            controller: vm.refreshController,
            onLoad: vm.loadMore,
            onRefresh: vm.viewRefresh,
            child: ListView.builder(
                itemCount: vm.list.length,
                itemBuilder: (ctx, index) => _item(vm.list[index])),
          ),
    );
  }

BaseViewOfState

针对某些页面需要保持当前滑动状态而必须用到StatefulWidget,对State进行封装。
基本使用跟BaseView类似。

/// 基类 state 扩展[StatefulWidget] 的 [State]
mixin BaseViewOfState<T extends StatefulWidget, VM extends BaseViewModel>
    on State<T> {

拿退货退款举例,因为页面涉及到列表所以需要保持页面滑动状态所以必须使用StatefulWidget
案例:

/// 退货或者退款
class _ReturnedRefund extends StatefulWidget {
  const _ReturnedRefund(
    this.isRefund, {
    Key key,
  }) : super(key: key);

  final bool isRefund;
  @override
  _ReturnedRefundState createState() => _ReturnedRefundState();
}

class _ReturnedRefundState extends State<_ReturnedRefund>
    with
        AutomaticKeepAliveClientMixin,
        BaseViewOfState<_ReturnedRefund, ReturnedRefundListVM> {
  @override
  bool get wantKeepAlive => true;
  @override
  ViewConfig<ReturnedRefundListVM> initConfig(BuildContext context) =>
      ViewConfig(vm: ReturnedRefundListVM(widget.isRefund));

  @override
  Widget vmBuild(BuildContext context, vm, Widget child, state) {
    return PSDisplay(
      primary: () => state,
      secondary: () => EasyRefresh(
        controller: vm.refreshController,
        onRefresh: vm.viewRefresh,
        onLoad: vm.loadMore,
        child: ListIntervalView(
          space: 32,
          itemCount: vm.list.length,
          itemBuilder: (_, index) => _OrderItem(vm, index),
        ),
      ),
    );
  }
}

写在最后

这是本人第一个认真整理并开源的框架,希望可以帮助到大家,让大家在日后做项目的时候能专心于业务的实现,啰啰嗦嗦写了一大堆,在思路上、代码水平等方面会有些不足,望大家提醒我会吸取教训并加以改进。

快速开发,自由配置,各种状态页,刷新颗粒度控制,兼容下拉刷新上拉加载,电匠工坊 作者线上项目使用中,安卓和苹果都已上架,放心使用。
有问题可以加我 QQ 275916180,欢迎来讨论。
项目在 GitHub上开源了,望大家点赞谢谢。

谢谢支持,请作者喝咖啡。
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Flutter Zoomable Image 是一个用于 Flutter 应用程序的库,它提供了一个可缩放和拖动的图像小部件。使用 Flutter Zoomable Image,您可以轻松地实现图像的缩放、拖动和捏放手势操作。这对于创建具有可交互性的图像查看器和画廊等应用程序非常有用。 要使用 Flutter Zoomable Image,您需要在项目的 `pubspec.yaml` 文件中添加依赖项,并运行 `flutter packages get` 命令来获取库。 以下是一个简单的示例代码,演示了如何在 Flutter 中使用 Zoomable Image: ```dart import 'package:flutter/material.dart'; import 'package:flutter_zoomable_image/flutter_zoomable_image.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Zoomable Image Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Zoomable Image Demo'), ), body: Center( child: ZoomableImage( AssetImage('path/to/your/image.jpg'), placeholder: Center(child: CircularProgressIndicator()), backgroundColor: Colors.black, ), ), ); } } ``` 在上面的示例中,我们创建了一个简单的 Flutter 应用程序,其中包含一个使用 ZoomableImage 小部件的页面。ZoomableImage 接受一个 AssetImage 对象作为图像源,并提供了一些可选参数,例如 placeholder(用于在图像加载期间显示的小部件)和 backgroundColor(用于设置图像背景色)。 您可以根据自己的需求定制 Zoomable Image 的样式和行为。要了解更多关于 Flutter Zoomable Image 的信息和用法,请参考官方文档或库的 GitHub 页面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值