阅读 4732

Flutter MVVM 简单实践

接着上一篇文章:基于MVVM架构封装Flutter基础库

这篇文章主要使用基础库对MVVM做简单实践:Demo地址

一、MVVM回顾

Model:数据模型

1、XApi
2、通常来说,Model中保存了相关业务的数据,负责提取和处理数据,数据来源可以是本地数据库,也可以来自网络;

View:视图

1、BaseView
2、View只做和UI相关的工作,不涉及任何业务逻辑,不涉及操作数据,不处理数据;
3、通俗讲就是展示给用户的界面及控件,比如Flutter中参与界面展示的Widget;

ViewModel:视图模型

1、BaseViewModel
2、ViewModel将View和Model进行解耦,并且实现View与Model的交互;
3、简单讲就是所有的业务逻辑都由它负责,而不是将业务逻辑和View都糅合在一起;

Data Binding:绑定器

1、Provider
2、View通过数据绑定来关心ViewModel的数据变化;
3、通过Provider的Consumer/Selector等组件来实现数据绑定;

二、基础库MVVM组件介绍

1、BaseView功能说明

通过Consumer实现View和ViewModel绑定,ViewModel数据变化时通知View刷新;
BaseView配合BaseViewModel使用,进入页面时通过BaseViewModel.onLoading()方法触发http请求;
BaseView通过FutureBuilder实现异步UI更新,从而实现http通用加载错误页,空白页以及正常显示页UI和逻辑;

///@date:  2021/3/1 13:38
///@author:  lixu
///@description:View 基类,配合[BaseViewModel]使用
///封装http加载错误页,空白页以及正常显示页UI和逻辑,UI可自定义
class BaseView<T extends BaseViewModel> extends StatefulWidget {
    ///加载成功后显示的页面
    final Widget child;
    
    ///加载中页面
    final Widget loadingChild;
    
    ///数据为空的页面
    final Widget emptyChild;
    
    ///请求失败显示的页面
    final Widget errorChild;

    BaseView({@required this.child, this.loadingChild, 
    this.emptyChild, this.errorChild}) : assert(child != null);

    @override
    _BaseViewState<T> createState() => _BaseViewState<T>();
}

class _BaseViewState<T extends BaseViewModel> extends State<BaseView> 
                                    with AutomaticKeepAliveClientMixin {
    @override
    Widget build(BuildContext context) {
        super.build(context);
        ///数据绑定
        return Consumer<T>(
            child: widget.child,
            builder: (BuildContext context, T viewModel, Widget child) {
                if (viewModel.isSuccess()) {
                    return child;
                } else {
                    ///异步UI更新
                    return FutureBuilder(
                        ///触发网络请求:BaseViewModel.onLoading(context)
                        future: viewModel.onLoading(context),
                        builder: (context, snapshot) {
                             if (snapshot.connectionState == 
                                          ConnectionState.done) {
                                if (viewModel.isFail()) {
                                    ///加载失败
                                    return _getErrorWidget(viewModel);
                                } else if (viewModel.isEmpty()) {
                                    ///数据为空
                                    return _getEmptyWidget(viewModel);
                                } else {
                                    ///加载成功
                                    return child;
                                }
                             } else {
                                ///加载中
                                return _getLoadingWidget();
                             }
                        },
                    );
                }
            },
       );
   }

    ...省略部分代码

    @override
    bool get wantKeepAlive => true;
}
复制代码

2、BaseCommonViewModel功能说明

ViewModel 顶层基类,进入View页面时,直接显示UI(不需要请求http获取数据)的 场景__继承BaseCommonViewModel类;

///@date:  2021/3/1 11:05
///@author:  lixu
///@description: ViewModel 基类
///进入View页面时,直接显示UI(不需要请求http获取数据)的场景继承[BaseCommonViewModel]类
abstract class BaseCommonViewModel with ChangeNotifier {
///是否已经调用了dispose()方法
bool _isDispose = false;

///是否正在请求中
bool isLoading = false;

///网络请求对象,充当MVVM的Model层
XApi api = XApi();

///获取tag,用于日志tag
String getTag();

///保存请求token,用于页面关闭时取消请求
List<CancelToken> cancelTokenList = [];

///刷新页面
@override
notifyListeners() {
    LogUtils.v(getTag(), 'notifyListeners() isDispose:$_isDispose');
    if (!_isDispose) {
        super.notifyListeners();
    }
}

bool get isDispose => _isDispose;

///页面关闭时回调该方式,释放资源
///使用ChangeNotifierProvider的默认构造方法来注入ViewModel,才能保证资源能正确释放
@override
void dispose() {
    super.dispose();
    ///页面关闭取消请求
    api?.cancelList(cancelTokenList);
    _isDispose = true;
    LogUtils.v(getTag(), 'dispose()');
}

}
复制代码

3、BaseViewModel功能说明

ViewModel 基类_,_进入页面时,需要http获取数据后才能显示UI的场景继承BaseViewModel类,配合 BaseView使用,实现http加载错误页,空白页以及正常显示页UI和逻辑;

///@date:  2021/3/1 11:09
///@author:  lixu
///@description: ViewModel基类
///进入View页面时,需要http获取数据后才能显示UI的场景继承[BaseViewModel]类,配合[BaseView]使用
///泛型T:进入页面时,http获取数据对象的类型
///1.列表加载(分页加载)
///2.单个对象加载
abstract class BaseViewModel<T> extends BaseCommonViewModel {

...省略其它代码

///接口获取的是否是列表数据,否则就是单个数据对象
///默认true
bool _isRequestListData;

///数据源:针对列表请求
List<T> dataList;

///数据源:针对单个对象请求
T dataBean;

///获取http请求参数
Map<String, dynamic> getRequestParams();

///获取http请求url
String getUrl();

///加载数据:进入页面时触发该方法
Future onLoading(BuildContext context) async {
    LogUtils.i(getTag(), 'onLoading');
    
    if (isLoading) {
      LogUtils.w(getTag(), 'onLoading() is Loading');
      return;
    }
    
    isLoading = true;
    _refreshedText = '刷新成功';
    CancelToken cancelToken = CancelToken();
    cancelTokenList.add(cancelToken);
    
    if (_isRequestListData) {
        ///请求列表数据
        await api.requestList<T>(
                getUrl(),
                params: getRequestParams(),
                isShowLoading: false,
                isShowFailToast: isShowFailToast(false),
                cancelToken: cancelToken,
                onSuccess: (List<T> list) {
                    ///请求成功回调
                    dataList = [];
                    list = list ?? [];
                    
                    ///解析json
                    dataList.addAll(list);
    
                    if (_isPageLoad) {
                        ///list分页加载
                        if (list.length < _pageSize) {
                            _canLoadMore = false;
                        } else {
                            _canLoadMore = true;
                            _pageNum++;
                        }
                    } else {
                        ///list没有分页加载
                        _canLoadMore = false;
                    }
                },
                onError: (HttpErrorBean errorBean) {
                    ///请求失败回调
                    _refreshedText = '刷新失败';
                    onErrorCallback(errorBean);
                },
                onComplete: () {
                    ///请求完成回调
                    isLoading = false;
                    cancelTokenList?.remove(cancelToken);
                },
            );    
    } else {
        ///请求单个数据对象
        await api.request<T>(
                getUrl(),
                params: getRequestParams(),
                isShowLoading: false,
                isShowFailToast: isShowFailToast(false),
                cancelToken: cancelToken,
                onSuccess: (T bean) {
                    ///请求成功回调
                    dataBean = bean;
                },
                onError: (HttpErrorBean errorBean) {
                    ///请求失败回调
                    _refreshedText = '刷新失败';
                    onErrorCallback(errorBean);
                },
                onComplete: () {
                    ///请求完成回调
                    isLoading = false;
                    cancelTokenList?.remove(cancelToken);
                },
            );
    }
}
        
      
///请求失败:重试刷新页面
void retryRefresh() {
    if (!isDispose) {
        notifyListeners();
    } else {
        LogUtils.e(getTag(), 'call retryRefresh() had dispose()');
    }
}

///请求是否成功
bool isSuccess() {
    if (_isRequestListData) {
        return dataList != null && dataList.length > 0;
    } else {
        return dataBean != null;
    }
}

///请求是否失败
bool isFail() {
    if (_isRequestListData) {
        return dataList == null;
    } else {
        return dataBean == null;
    }
}

///请求数据是否为空
bool isEmpty() {
    if (_isRequestListData) {
        return dataList == null || dataList.isEmpty;
    } else {
        return dataBean == null;
    }
}

...省略其它代码

}
复制代码

三、MVVM实践

基于下面场景: 

 1. 打开用户列表页面,调用接口获取用户信息成功后显示用户列表UI;

 2. 调用接口失败或数据为空,显示对应的占位页面,占位UI可以自定义; 

 3. 请求未完成时关闭页面,自动取消请求;

 1、Model(UserDetailBean和XApi)

///@date:  2021/3/2 11:20
///@author:  lixu
///@description: 用户详情对象
class UserDetailBean {
    ///头像
    String icon;
    
    ///用户id
    String userId;
    
    ///用户名
    String name;
    
    UserDetailBean.fromJsonMap(Map<String, dynamic> map)
          : userId = map["userId"]?.toString(),
    name = map["name"],
    icon = map["icon"];
}
复制代码

 2、View(UserPage)

///@date:  2021/3/11
///@author:  lixu
///@description:用户列表页面
///进入页面时调用接口(用户列表)获取数据成功后,才能显示UI
class UserPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: Text('用户列表页面(MVVM)')),
            ///使用ChangeNotifierProvider的默认构造方法来注入ViewModel,
            ///保证资源能正确释放
            body: ChangeNotifierProvider(
                create: (_) {
                    return UserListViewModel();
                },    
                child: BaseView<UserListViewModel>(
                    ///UserListView为请求成功后显示的View(用户列表)
                    child: UserListView(),
                    ///自定义请求失败的View
                    ///如果不设置,会使用全局失败页面(IResConfig中配置的资源)
                    errorChild: Center(
                          child: Text(
                            '这是自定义请求失败的页面:\n请求失败,点击重试',
                            style: TextStyle(color: Colors.red, 
                                             fontSize: 20),
                            textAlign: TextAlign.center,
                          ),
                    ),
                 ),
           ),
        );
    }
}
复制代码

3、ViewModel(UserListViewModel)

///@date:  2021/3/2 11:26
///@author:  lixu
///@description: 用户列表viewModel
///泛型[UserDetailBean]:进入页面时,http获取数据对象的类型
class UserListViewModel extends BaseViewModel<UserDetailBean> {

    UserListViewModel() : super(isPageLoad: false);

    ///获取http请求参数
    @override
    Map<String, dynamic> getRequestParams() {
        return {
            'userId': loginInfo.userBean?.userId,
            'token': loginInfo.token,
        };
    }
    
    @override
    String getTag() {
        return 'UserListViewModel';
    }
    
    ///获取http请求url
    @override
    String getUrl() {
        return HttpUrls.userListUrl;
    }

}
复制代码

4、思考下面场景

       上面mvvm 实践,进入页面时需要调用一个接口获取数据成功后才显示UI,如果需要调用多个接口成功后才能显示UI,该如何处理?

       建议对每个接口封装独立的ViewModel来处理逻辑,参考代码如下:

///@date:  2021/3/11 
///@author:  lixu 
///@description:用户列表页面2 
///进入页面时调用2(多个)个接口获取数据成功后,才能显示UI 
///1、获取token接口[TokenViewModel]  
///2、用户列表接口[UserListViewModel] 
class UserPage extends StatelessWidget { 
  @override 
  Widget build(BuildContext context) { 
    return Scaffold( 
      appBar: AppBar(title: Text('用户列表页面(MVVM)')), 
      body: MultiProvider( 
        providers: [ 
          ChangeNotifierProvider(create: (_) { 
            return TokenViewModel(); 
          }), 
          ChangeNotifierProvider(create: (_) { 
            return UserListViewModel(); 
          }), 
        ], 
        ///1、TokenViewModel 获取token接口
        child: BaseView<TokenViewModel>( 
            ///2、UserListViewModel 获取用户信息的接口
            child: BaseView<UserListViewModel>( 
              ///请求成功显示的UI
              child: UserListView(), 
              ///数据为空的UI
              emptyChild: Center( 
                child: Text( 
                  '这是自定义请求数据为空的页面:\n数据为空,点击刷新', 
                  style: TextStyle(color: Colors.blue, fontSize: 20), 
                  textAlign: TextAlign.center, 
                ), 
              ), 
            ), 
         ), 
      ), 
    ); 
  } 
}
复制代码

       如果第一个接口的响应要作为第二个接口的请求参数,该如何处理?参考这里

四、总结

       通过使用:BaseView+BaseViewModel+Provider+XApi 对MVVM进行了简单的实现,更多功能可以下载demo体验

上一篇文章:Flutter Dio封装实践

文章分类
Android
文章标签