目錄
一、最原始的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();
}
}