lzyprime 博客
λ:
這篇文章是對 “ 19年11月的文章 ”的補充, 就文章里用到的 rxdart ^0.22.2 與最新版本 rxdart ^0.23.1 出現(xiàn)了大改動,以至于原文中的代碼在最新插件下不可用。。。
這次插件更新, 相當于給原生 Stream 類添加了一組拓展方法,而不是像以前一樣用 Observable 類再包一層達到效果。(詳情rxdart的更新日志)
也就是說,添加完插件后,你可以在任意Stream 調(diào)用一系列方法。如之前demo里用到的doOnData doOnDone doOnListen zipWith。還有更多內(nèi)容可看官網(wǎng)詳情
github 倉庫: flutter_demos 分支 mvvm_demo
git clone -b mvvm_demo https://github.com/lzyprime/flutter_demos.git
mvvm
mvvm 在 原文章 大概介紹過,而且架構(gòu)這種東西無關(guān)語言。具體是什么,相信wiki說的比我好。
flutter項目 mvvm
1. 添加插件:provider ^4.0.1(rxdart ^0.23.1 PS: 如果用不到 rxdart 的東西,可以不要這個插件)。 在 pubspec.yaml 文件

pubspec.png
2. view, 構(gòu)建UI,數(shù)據(jù)來源于viewModel
// file path: package:flutter_mvvm_demo/views/login_widget.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_mvvm_demo/viewModels/login_view_model.dart';
class LoginWidget extends StatelessWidget {
@override
build(BuildContext context) {
final provider = Provider.of<LoginViewModel>(context);
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: provider.usernameController,
decoration: InputDecoration(labelText: "username"),
),
TextField(
controller: provider.passwordController,
decoration: InputDecoration(labelText: "password"),
),
RaisedButton(
onPressed: provider.login,
/// 根據(jù) state 的值,按鈕顯示不同內(nèi)容。
child: provider.state == 0
? Text("login")
: provider.state == 1
? CircularProgressIndicator()
: provider.state == 2
? Icon(Icons.done)
: Icon(Icons.cancel),
),
],
),
);
}
}
3. model, 請求和處理
// file path: package:flutter_mvvm_demo/models/login_model.dart
class LoginModel {
/// 直接將網(wǎng)絡(luò)請求的 Future 對象包裝成 Stream 返回
/// Stream.fromFuture 等構(gòu)造方法。更多細節(jié)參考官方文檔
/// 因為是 demo 所以用 Future.delayed 模擬請求過程
Stream<int> login(dynamic data) => Stream.fromFuture(
Future.delayed(Duration(seconds: 2), () {
if (data["username"] == "lzyprime" && data["password"] == "123")
return 0;
return -1;
}),
);
}
4. viewModel
// file path: package:flutter_mvvm_demo/view_models/login_view_model.dart
import 'package:flutter/material.dart';
// import 'package:rxdart/rxdart.dart'; 如果需要,自行添加插件
import 'package:flutter_mvvm_demo/models/login_model.dart';
/// with ChangeNotifier : 通過 notifyListeners() 函數(shù),可以通知本對象數(shù)據(jù)的正在使用者們。
/// 如 state 變量,在改變后調(diào)用 notifyListeners(), UI根據(jù)值重新構(gòu)建登錄按鈕顯示內(nèi)容
class LoginViewModel with ChangeNotifier {
final _model = LoginModel();
int state = 0; // 0 未請求,1 正在請求, 2 請求成功, 3請求失敗
final usernameController = TextEditingController();
final passwordController = TextEditingController();
login() {
final data = {
"username": usernameController.text,
"password": passwordController.text,
};
/// 不為 0 說明上一條請求未完成,直接退出
if (state != 0) return;
/// 開始請求,state 賦值為 1, 并通知監(jiān)聽者
/// 如果用rxDart插件,可作為doOnListen參數(shù)的函數(shù)體
state = 1;
notifyListeners();
_model.login(data)
/// rxDart 插件
// .doOnListen(() {
// state = 1;
// notifyListeners();
// })
.listen((v) {
if (v != 0) {
/// 返回值不為0,請求失敗
state = 3;
notifyListeners();
Future.delayed(Duration(seconds: 1), () {
state = 0;
notifyListeners();
});
} else {
/// 返回值為0,請求成功
state = 2;
notifyListeners();
Future.delayed(Duration(seconds: 1), () {
state = 0;
notifyListeners();
});
}
});
}
}
組裝三者
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_mvvm_demo/views/login_widget.dart';
import 'package:flutter_mvvm_demo/viewModels/login_view_model.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter mvvm Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
/// [ChangeNotifierProvider]。所有的viewModel通過 Provider 實現(xiàn)與view 層的綁定。
/// Provider 是對 [InheritedWidget] 封裝。因此我們才能實現(xiàn)調(diào)用notifyListeners() 時,通知子樹重新構(gòu)建
/// 當然你也可以一個插件也不用,自己封裝[InheritedWidget]
home: ChangeNotifierProvider(
create: (_) => LoginViewModel(),
child: LoginWidget(),
),
);
}
}
效果圖
-
state == 0, 未請求

state0.png
-
state == 1, 顯示加載

state1.png
-
state == 2, 請求成功

state2.png
-
state == 3, 請求失敗

state3.png
~λ:
這只是個demo, 主要看三者怎么拆以及怎么拼裝,實際請求肯定還要判好多問題。
實際應(yīng)用時,model組織api請求,而一個頁面可能涉及多個模塊的請求,因此,一個 viewModel 包含多個 model 類也有可能。但盡量避免這種情況,做好請求分類