Flutter MVVM 簡單實踐

隔壁的李叔叔發表於2021-03-13

接著上一篇文章:基於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、BaseViewModel功能說明

進入頁面時,需要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;
    }
}

...省略其它程式碼

}
複製程式碼

3、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()');
}

}
複製程式碼

三、MVVM實踐

 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;
    }

}
複製程式碼

四、總結

       通過使用:BaseView+BaseViewModel+Provider+XApi 對MVVM進行了簡單的實現,更多功能可以下載demo體驗

相關文章