目錄
一、最原始的MVVM
二彭则、使用Listener的MVVM
三鳍刷、使用Provider的MVVM
四、使用GetX的MVVM
需求很簡單:
- 搜索框輸入“張”時就請求前綴為“張”的用戶數(shù)據(jù)俯抖、同時也請求后綴為“張”的用戶數(shù)據(jù)输瓜,搜索框輸入“李”時就請求前綴為“李”的用戶數(shù)據(jù)、同時也請求后綴為“李”的用戶數(shù)據(jù)
- 請求完成后芬萍,前綴數(shù)據(jù)交給上面的ListView顯示尤揣,后綴數(shù)據(jù)交給下面的ListView顯示
一、最原始的MVVM
- Model層
-----------person_model.dart-----------
/*
Model的職責:Model只負責封裝數(shù)據(jù)柬祠,不做任何其它操作北戏。
*/
/// Person模型
class PersonModel {
/// 普通構(gòu)造方法
PersonModel({
this.name,
this.sex,
this.age,
});
/// 姓名
String? name;
/// 性別
///
/// 0-未知,1-男漫蛔,2-女
int? sex;
/// 年齡
int? age;
/// 工廠構(gòu)造方法
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"],
);
/// 模型轉(zhuǎn)字典
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負責響應(yīng)與業(yè)務(wù)有關(guān)的事件并交給Controller去處理嗜愈,怎么交給Controller呢?通過閉包莽龟、通知等芝硬。
*/
import 'package:flutter/material.dart';
/// 搜索框Widget
class SearchBarWidget extends StatelessWidget {
SearchBarWidget({
this.searchTextDidChangeCallback,
});
/// 搜索內(nèi)容改變的回調(diào)
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負責顯示數(shù)據(jù),那怎么顯示數(shù)據(jù)呢轧房?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負責顯示數(shù)據(jù)绍绘,那怎么顯示數(shù)據(jù)呢奶镶?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, // 加載成功陪拘,但數(shù)據(jù)為空
failure, // 加載失敗
noNetwork, // 沒網(wǎng)
}
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負責獲取數(shù)據(jù);
2左刽、ViewModel負責處理數(shù)據(jù)捺信;
3、ViewModel負責存儲數(shù)據(jù)。
*/
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 {
// 命名構(gòu)造方法迄靠,專門用來初始化_personModel
PersonViewModel._from(PersonModel? personModel) {
_personModel = personModel;
}
// 持有一個_personModel秒咨,以便處理數(shù)據(jù):ViewModel一對一Model地添加屬性并處理,搞成getter方法即可
PersonModel? _personModel;
/// 普通構(gòu)造方法
PersonViewModel();
/// 存儲數(shù)據(jù):vm數(shù)組
///
/// 真正暴露給外面使用的是vm數(shù)組掌挚,里面的數(shù)據(jù)已經(jīng)處理好了雨席,直接拿著顯示就行了
List<PersonViewModel> prefixPersonViewModelList = [];
List<PersonViewModel> suffixPersonViewModelList = [];
/// 存儲數(shù)據(jù):錯誤消息
String errorMsg = "";
/// 處理數(shù)據(jù):姓名
String get name {
return _personModel?.name ?? "";
}
/// 處理數(shù)據(jù):性別
///
/// 0-未知,1-男吠式,2-女
String get sex {
if (_personModel?.sex == 1) {
return "男";
} else if (_personModel?.sex == 2) {
return "女";
} else {
return "未知";
}
}
/// 處理數(shù)據(jù):年齡
int get age {
return _personModel?.age ?? 0;
}
/// 請求前綴數(shù)據(jù)
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);
}
}
/// 請求后綴數(shù)據(jù)
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,創(chuàng)建View特占,并把View添加到窗口上顯示糙置;
2、Controller負責持有ViewModel是目,調(diào)用ViewModel的方法去請求數(shù)據(jù)谤饭;
3、vm --> view:Controller調(diào)用vm的方法請求數(shù)據(jù)胖笛,請求完成后vm是通過回調(diào)的方式告訴Controller的:
請求成功后Controller需要調(diào)用一下setState來刷一下UI网持,這樣view就會去拿vm里最新存儲的數(shù)據(jù)來展示了
請求失敗后Controller可以toast一下錯誤信息給用戶看,或者調(diào)用一下setState來刷一下UI长踊,刷成暫無數(shù)據(jù)那種view
4功舀、view --> vm:view產(chǎn)生的變化是通過回調(diào)告訴Controller的,Controller可以調(diào)用vm的方法把view發(fā)生的變化告訴它
*/
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調(diào)用vm的方法請求數(shù)據(jù),請求完成后vm通過什么方式告訴Controller淮逊,之前是通過回調(diào)的方式催首,現(xiàn)在是通過Listener的方式】。
它的優(yōu)勢就是【界面退出時假如我們的請求還沒走完泄鹏,會自動移除監(jiān)聽】郎任,也就是說就算網(wǎng)絡(luò)請求完了也不會再觸發(fā)Controller里的監(jiān)聽了,我們壓根兒不需要關(guān)心這個界面在不在視圖樹里备籽,只需要做業(yè)務(wù)即可舶治。【它的劣勢就是因為所有的業(yè)務(wù)都會觸發(fā)同一個監(jiān)聽而使得代碼判斷起來復雜了,之前起碼是一個業(yè)務(wù)一個回調(diào)、各管各的】霉猛,它給人感覺上好像是在數(shù)據(jù)驅(qū)動UI尺锚,但其實并不是,因為這里并不存在數(shù)據(jù)和UI的綁定操作韩脏,本質(zhì)上還是數(shù)據(jù)變化后缩麸、我們手動刷新UI來展示最新的數(shù)據(jù)。
因此相對于最原始的MVVM來說赡矢,更推薦最原始的MVVM杭朱。
- Model層和View層都不需要改動
- ViewModel層
-----------base_view_model.dart-----------
import 'package:flutter/cupertino.dart';
enum ViewState {
loading, // 加載中
success, // 加載成功
empty, // 加載成功,但數(shù)據(jù)為空
failure, // 加載失敗
noNetwork, // 沒網(wǎng)
}
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負責獲取數(shù)據(jù)弧械;
2、ViewModel負責處理數(shù)據(jù)空民;
3刃唐、ViewModel負責存儲數(shù)據(jù)。
*/
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里所做的業(yè)務(wù)
class PersonViewModelService {
static const String loadPrefixData = "loadPrefixData";
static const String loadSuffixData = "loadSuffixData";
}
/// PersonViewModel里所做業(yè)務(wù)的結(jié)果
class PersonViewModelServiceResult {
PersonViewModelServiceResult({
required this.service,
required this.isSuccess,
});
/// 具體是哪個業(yè)務(wù)
String service;
/// 結(jié)果
bool isSuccess;
}
/// Person視圖模型
///
/// 要想使用Listener界轩,PersonViewModel得繼承自或者混入ChangeNotifier
class PersonViewModel extends BaseViewModel {
late PersonViewModelServiceResult serviceResult;
// 命名構(gòu)造方法画饥,專門用來初始化_personModel
PersonViewModel._from(PersonModel? personModel) {
_personModel = personModel;
}
// 持有一個_personModel,以便處理數(shù)據(jù):ViewModel一對一Model地添加屬性并處理浊猾,搞成getter方法即可
PersonModel? _personModel;
/// 普通構(gòu)造方法
PersonViewModel();
/// 存儲數(shù)據(jù):vm數(shù)組
///
/// 真正暴露給外面使用的是vm數(shù)組抖甘,里面的數(shù)據(jù)已經(jīng)處理好了,直接拿著顯示就行了
List<PersonViewModel> prefixPersonViewModelList = [];
List<PersonViewModel> suffixPersonViewModelList = [];
/// 存儲數(shù)據(jù):錯誤消息
String errorMsg = "";
/// 處理數(shù)據(jù):姓名
String get name {
return _personModel?.name ?? "";
}
/// 處理數(shù)據(jù):性別
///
/// 0-未知葫慎,1-男衔彻,2-女
String get sex {
if (_personModel?.sex == 1) {
return "男";
} else if (_personModel?.sex == 2) {
return "女";
} else {
return "未知";
}
}
/// 處理數(shù)據(jù):年齡
int get age {
return _personModel?.age ?? 0;
}
/// 請求前綴數(shù)據(jù)
///
/// 請求完成后,本來是通過回調(diào)告訴Controller的偷办,現(xiàn)在不要回調(diào)了艰额,通過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();
}
}
/// 請求后綴數(shù)據(jù)
///
/// 請求完成后,本來是通過回調(diào)告訴Controller的椒涯,現(xiàn)在不要回調(diào)了柄沮,通過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废岂,創(chuàng)建View铡溪,并把View添加到窗口上顯示;
2泪喊、Controller負責持有ViewModel,調(diào)用ViewModel的方法去請求數(shù)據(jù)髓涯;
3袒啼、vm --> view:Controller調(diào)用vm的方法請求數(shù)據(jù),請求完成后vm是通過Listener而非回調(diào)的方式告訴Controller的:
請求成功后Controller需要調(diào)用一下setState來刷一下UI,這樣view就會去拿vm里最新存儲的數(shù)據(jù)來展示了
請求失敗后Controller可以toast一下錯誤信息給用戶看蚓再,或者調(diào)用一下setState來刷一下UI滑肉,刷成暫無數(shù)據(jù)那種view
4、view --> vm:view產(chǎn)生的變化是通過回調(diào)告訴Controller的摘仅,Controller可以調(diào)用vm的方法把view發(fā)生的變化告訴它
*/
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添加監(jiān)聽
//
// 它里面任何時候靶庙、任何地方發(fā)出notifyListeners,都會觸發(fā)這里添加好的監(jiān)聽
// 那它里面什么時候娃属、什么地方發(fā)出notifyListeners呢六荒?當然就是請求完成的時候,在請求完成的回調(diào)里發(fā)出
// 當然因為它內(nèi)部可能會做多個業(yè)務(wù)矾端,如果多個業(yè)務(wù)都發(fā)出了notifyListeners掏击,則都會觸發(fā)這里的同一個回調(diào),因此我們會在它里面添加一個類來區(qū)分到底是哪個業(yè)務(wù)完成了秩铆,以便在監(jiān)聽里處理不同的業(yè)務(wù)
_personViewModel.addListener(_personViewModelListener);
super.initState();
}
// 第二步:_personViewModel處理監(jiān)聽
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移除監(jiān)聽
_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這個框架的首要功能是數(shù)據(jù)共享,所以我們這里使用Provider的首要目的就是用它來共享_personViewModel這個數(shù)據(jù)殴玛,因為我們的ListPage捅膘、PrefixListViewWidget和SuffixListViewWidget都使用到了同一個_personViewModel,當然我們的例子可能比較簡單滚粟,【實際開發(fā)中你的一個界面里可能會由很多很多個Widget組成寻仗,而它們可能都需要使用同一個viewModel,甚至多個界面之間也需要使用同一個viewModel坦刀,那最原始的寫法就是像最原始的MVVM里那樣通過指針傳遞viewModel來共享愧沟,而Provider則提供了另外一種共享數(shù)據(jù)的方式——只要一堆Widget擁有同一個Provider作為父視圖,那么這些Widget就都可以共享這個Provider綁定的viewModel數(shù)據(jù)】鲤遥,這就是它相對于最原始MVVM的第一個變化沐寺,這個變化主要解決了viewModel傳來傳去的問題。(如果你只想一個界面內(nèi)的多個Widget共享數(shù)據(jù)盖奈,那么這多個Widget的父視圖就必須得是這個Provider混坞,所以你可以把Provider包在這個界面上即可;如果你想多個界面之間共享數(shù)據(jù)钢坦,那么這多個界面的父視圖就必須得是這個Provider究孕,因此這個時候我們會把Provider包在App的最底層)
實現(xiàn)了上面的內(nèi)容之后其實已經(jīng)完成了Provider數(shù)據(jù)共享的功能,【但是我們會發(fā)現(xiàn)使用Provider時爹凹,它要求我們的ViewModel必須繼承自或混入ChangeNotifier厨诸,這是因為Provider的第二個功能就是局部刷新,也就是說我們只需要在ViewModel里合適的時機禾酱、合適的地方發(fā)出一個notifyListeners微酬,Provider就會自動觸發(fā)它Consumer或Selector的回調(diào)來只刷新局部UI來展示最新的數(shù)據(jù)绘趋、當然我們也可以在些回調(diào)里Toast錯誤消息等。同時我們也只需要在ViewModel里發(fā)notifyListeners就行了,也不用像使用Listener那樣考慮到底是什么業(yè)務(wù)完成了而做一堆判斷,Controller里也沒有什么監(jiān)聽蛾茉,因為各個監(jiān)聽已經(jīng)分散到了各個Consumer或Selector回調(diào)那里】,這就是它相對于最原始MVVM的第二個變化帽馋,這個變化主要解決了ViewModel怎么把變化告訴Controller的問題。
【它的優(yōu)勢就是更加簡單的數(shù)據(jù)共享方式 + 響應(yīng)式編程數(shù)據(jù)驅(qū)動UI比吭≌雷澹】【它的劣勢就是Provider框架的侵入性太強了,而且代碼編寫起來有點費勁梗逮∠畋】
因此相對于最原始的MVVM來說,你有余力的話可以學學Provider并應(yīng)用在你的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() {
// 第一步:創(chuàng)建數(shù)據(jù)存放地娄蔼,也就是我們想共享的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, // 加載成功底哗,但數(shù)據(jù)為空
failure, // 加載失敗
noNetwork, // 沒網(wǎng)
}
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負責獲取數(shù)據(jù);
2跋选、ViewModel負責處理數(shù)據(jù)涕癣;
3、ViewModel負責存儲數(shù)據(jù)前标。
*/
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 {
// 命名構(gòu)造方法,專門用來初始化_personModel
PersonViewModel._from(PersonModel? personModel) {
_personModel = personModel;
}
// 持有一個_personModel炼列,以便處理數(shù)據(jù):ViewModel一對一Model地添加屬性并處理只搁,搞成getter方法即可
PersonModel? _personModel;
/// 普通構(gòu)造方法
PersonViewModel();
/// 存儲數(shù)據(jù):vm數(shù)組
///
/// 真正暴露給外面使用的是vm數(shù)組,里面的數(shù)據(jù)已經(jīng)處理好了俭尖,直接拿著顯示就行了
List<PersonViewModel> prefixPersonViewModelList = [];
List<PersonViewModel> suffixPersonViewModelList = [];
/// 存儲數(shù)據(jù):錯誤消息
String errorMsg = "";
/// 處理數(shù)據(jù):姓名
String get name {
return _personModel?.name ?? "";
}
/// 處理數(shù)據(jù):性別
///
/// 0-未知氢惋,1-男,2-女
String get sex {
if (_personModel?.sex == 1) {
return "男";
} else if (_personModel?.sex == 2) {
return "女";
} else {
return "未知";
}
}
/// 處理數(shù)據(jù):年齡
int get age {
return _personModel?.age ?? 0;
}
/// 請求前綴數(shù)據(jù)
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;
}
}
/// 請求后綴數(shù)據(jù)
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負責顯示數(shù)據(jù)稽犁,那怎么顯示數(shù)據(jù)呢焰望?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的情況):在需要使用共享數(shù)據(jù)的Widget外包一層Consumer或者Selector來使用共享數(shù)據(jù)已亥,需要get共享數(shù)據(jù)的地方使用Consumer
// Consumer是個泛型熊赖,泛的型就是問要使用哪個viewModel里的數(shù)據(jù),填進去即可
child: Consumer<PersonViewModel>(
// Consumer有一個必添屬性builder虑椎,接收一個函數(shù)作為屬性值震鹉,該函數(shù)的第二個參數(shù)就是共享數(shù)據(jù)存放地
// 當數(shù)據(jù)發(fā)生變化時的妖,就會觸發(fā)這個builder函數(shù)來刷新Widget,所以我們在這個函數(shù)里返回原始的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負責顯示數(shù)據(jù),那怎么顯示數(shù)據(jù)呢娇未?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的情況):在需要使用共享數(shù)據(jù)的Widget外包一層Consumer或者Selector來使用共享數(shù)據(jù),需要get共享數(shù)據(jù)的地方使用Consumer
// Consumer是個泛型零抬,泛的型就是問要使用哪個viewModel里的數(shù)據(jù)镊讼,填進去即可
child: Consumer<PersonViewModel>(
// Consumer有一個必添屬性builder,接收一個函數(shù)作為屬性值平夜,該函數(shù)的第二個參數(shù)就是共享數(shù)據(jù)存放地
// 當數(shù)據(jù)發(fā)生變化時蝶棋,就會觸發(fā)這個builder函數(shù)來刷新Widget,所以我們在這個函數(shù)里返回原始的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,創(chuàng)建View段直,并把View添加到窗口上顯示吃溅;
2、Controller負責持有ViewModel鸯檬,調(diào)用ViewModel的方法去請求數(shù)據(jù)决侈;
3、vm --> view:Controller調(diào)用vm的方法請求數(shù)據(jù)喧务,請求完成后vm是通過回調(diào)的方式告訴Controller的:
請求成功后Controller需要調(diào)用一下setState來刷一下UI赖歌,這樣view就會去拿vm里最新存儲的數(shù)據(jù)來展示了
請求失敗后Controller可以toast一下錯誤信息給用戶看,或者調(diào)用一下setState來刷一下UI功茴,刷成暫無數(shù)據(jù)那種view
4庐冯、view --> vm:view產(chǎn)生的變化是通過回調(diào)告訴Controller的,Controller可以調(diào)用vm的方法把view發(fā)生的變化告訴它
*/
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的情況):在需要使用共享數(shù)據(jù)的Widget外包一層Consumer或者Selector來使用共享數(shù)據(jù)痊土,需要set共享數(shù)據(jù)的地方使用Selector
// Selector是個泛型肄扎,而且還泛了兩個型,第一個泛的型是問要使用哪個viewModel里的數(shù)據(jù)赁酝,填進去即可犯祠,第二個泛的型是轉(zhuǎn)換之后的數(shù)據(jù)類型,比如我這里轉(zhuǎn)換之后依然是使用PersonViewModel酌呆,那么我們填一樣的類型即可(大多數(shù)情況都不需要轉(zhuǎn)吧)
return Selector<PersonViewModel, PersonViewModel>(
// Selector有一個必添屬性selector衡载,接收一個函數(shù)作為屬性值,該函數(shù)的第二個參數(shù)就是共享數(shù)據(jù)存放地
// 該函數(shù)用來指定Selector的兩個泛型之間如何進行數(shù)據(jù)轉(zhuǎn)換隙袁,這里我們不轉(zhuǎn)換痰娱,所以返回原先的personViewModel
// 如果轉(zhuǎn)換后弃榨,則下面builder函數(shù)真正渲染W(wǎng)idget時的第二個參數(shù)就是轉(zhuǎn)換后的ViewModel了,不轉(zhuǎn)換的話builder函數(shù)的第二個參數(shù)就是原始的ViewModel
selector: (ctx, personViewModel) {
return personViewModel;
},
// Selector有一個必添屬性builder梨睁,接收一個函數(shù)作為屬性值鲸睛,該函數(shù)的第二個參數(shù)就是共享數(shù)據(jù)存放地
// 當數(shù)據(jù)發(fā)生變化時,就會觸發(fā)這個builder函數(shù)來刷新Widget坡贺,所以我們在這個函數(shù)里返回原始的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);
},
),
);
},
// 當共享數(shù)據(jù)發(fā)生變化時,是否執(zhí)行builder方法重新構(gòu)建Widget遍坟,我們可以設(shè)定為 return pre = next拳亿,不一樣時才刷新,一樣就不刷新
// 但是因為本次案例里是僅僅set共享數(shù)據(jù)愿伴,不需要get變化后的共享數(shù)據(jù)來刷新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支持響應(yīng)式編程隔节,使用它可以非常簡單地實現(xiàn)數(shù)據(jù)驅(qū)動UI的效果鹅经,只需要兩步:一在ViewModel里把外界想監(jiān)聽的數(shù)據(jù)通過
.obs
搞成Observable,二在外界想使用數(shù)據(jù)的地方通過Obx(() => Widget)
包裹真正的Widget搞成Observer官帘,這樣就可以數(shù)據(jù)驅(qū)動UI了瞬雹,這就是它相對于最原始MVVM的第一個變化,這個變化主要解決了ViewModel怎么把變化告訴Controller的問題刽虹。GetX也支持數(shù)據(jù)共享酗捌,也只需要兩步:一在Controller里本來創(chuàng)建_viewModel的地方
Get.put
一下,二完事就可以在任何想使用_viewModel的地方Get.find
到它來使用了涌哲,這就是它相對于最原始MVVM的第二個變化胖缤,這個變化主要解決了viewModel傳來傳去的問題。【它的優(yōu)勢就是非常簡單地響應(yīng)式編程數(shù)據(jù)驅(qū)動UI + 非常簡單得數(shù)據(jù)共享方式阀圾,比RxDart簡單地多哪廓。】【它的劣勢就是GetX框架的侵入性太強了初烘∥姓妫】
因此相對于最原始的MVVM來說,你有余力的話可以學學GetX并應(yīng)用在你的MVVM中肾筐;相對于使用Provider的MVVM來說哆料,則強烈推薦使用GetX,它的使用簡直太簡單了吗铐。
當然GetX還提供了很多其它的功能东亦,如相對于系統(tǒng)自帶的Navigator更加簡單地路由管理、App國際化等唬渗,可以根據(jù)自己的情況選擇使用典阵。
- Model層不需要改動
- ViewModel層
-----------base_view_model.dart-----------
import 'package:get/get.dart';
enum ViewState {
loading, // 加載中
success, // 加載成功
empty, // 加載成功奋渔,但數(shù)據(jù)為空
failure, // 加載失敗
noNetwork, // 沒網(wǎng)
}
class BaseViewModel extends GetxController {
Rx<ViewState> state = ViewState.success.obs;
}
-----------person_view_model.dart-----------
/*
ViewModel的職責:
1、ViewModel負責獲取數(shù)據(jù)壮啊;
2嫉鲸、ViewModel負責處理數(shù)據(jù);
3歹啼、ViewModel負責存儲數(shù)據(jù)充坑。
*/
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 {
// 命名構(gòu)造方法,專門用來初始化_personModel
PersonViewModel._from(PersonModel? personModel) {
_personModel = personModel;
}
// 持有一個_personModel染突,以便處理數(shù)據(jù):ViewModel一對一Model地添加屬性并處理,搞成getter方法即可
PersonModel? _personModel;
/// 普通構(gòu)造方法
PersonViewModel();
/// 存儲數(shù)據(jù):vm數(shù)組
///
/// 真正暴露給外面使用的是vm數(shù)組辈灼,里面的數(shù)據(jù)已經(jīng)處理好了份企,直接拿著顯示就行了
List<PersonViewModel> prefixPersonViewModelList = [];
List<PersonViewModel> suffixPersonViewModelList = [];
/// 存儲數(shù)據(jù):錯誤消息
String errorMsg = "";
/// 處理數(shù)據(jù):姓名
String get name {
return _personModel?.name ?? "";
}
/// 處理數(shù)據(jù):性別
///
/// 0-未知,1-男巡莹,2-女
String get sex {
if (_personModel?.sex == 1) {
return "男";
} else if (_personModel?.sex == 2) {
return "女";
} else {
return "未知";
}
}
/// 處理數(shù)據(jù):年齡
int get age {
return _personModel?.age ?? 0;
}
/// 請求前綴數(shù)據(jù)
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;
}
}
/// 請求后綴數(shù)據(jù)
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負責顯示數(shù)據(jù)司志,那怎么顯示數(shù)據(jù)呢?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負責顯示數(shù)據(jù)骂远,那怎么顯示數(shù)據(jù)呢?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,創(chuàng)建View额嘿,并把View添加到窗口上顯示瘸恼;
2、Controller負責持有ViewModel册养,調(diào)用ViewModel的方法去請求數(shù)據(jù)东帅;
3、vm --> view:Controller調(diào)用vm的方法請求數(shù)據(jù)球拦,請求完成后vm是通過回調(diào)的方式告訴Controller的:
請求成功后Controller需要調(diào)用一下setState來刷一下UI靠闭,這樣view就會去拿vm里最新存儲的數(shù)據(jù)來展示了
請求失敗后Controller可以toast一下錯誤信息給用戶看,或者調(diào)用一下setState來刷一下UI坎炼,刷成暫無數(shù)據(jù)那種view
4愧膀、view --> vm:view產(chǎn)生的變化是通過回調(diào)告訴Controller的,Controller可以調(diào)用vm的方法把view發(fā)生的變化告訴它
*/
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();
}
}