那今天就來讓這個非同步資料透過MobX 來串接到畫面上:
首先一樣建立一個 UsersViewModel
import 'dart:convert';
import 'package:flutter_rest_api_playground/model/users/users.dart';
import 'package:flutter_rest_api_playground/service/http.dart';
import 'package:mobx/mobx.dart';
part 'users_view_model.g.dart';
class UsersViewModel = _UsersViewModelBase with _$UsersViewModel;
abstract class _UsersViewModelBase with Store {
@observable
ObservableFuture<String>? foo;
@action
Future fetchFoo() {
return foo = ObservableFuture(Future.delayed(
const Duration(seconds: 3),
() => 'foo',
));
}
}
但在正式搬移 fetch users 相關的行為之前,我們先來看看MobX到底如何操作非同步事件
我們宣告一個 observable: ObservableFuture <String>? foo;
,這個ObservableFuture
類似一個可以被監聽的future,當他的非同步事件的狀態改變時會通知給監聽者。
然後我們需要一個action回傳一個 ObservableFuture
大部分就跟之前一樣將 usersViewModel
實例化出來,然後我們在 initState
這裡做 usersViewModel.fetchUserList()
class _MyHomePageState extends State<MyHomePage> {
UsersViewModel usersViewModel = UsersViewModel();
@override
void initState() {
usersViewModel.fetchFoo();
super.initState();
}
// 省略以下
}
那什麼是 initState
?在 Flutter 的widget有各種生命週期,所謂生命週期就是widget從建立到銷毀的各種狀況。
而initState
這個 State
是在初始化時會執行的一個function ,詳細的 statefulWidget
的生命週期可以參考文章最後面的連結。
Observer(builder: (_) {
final future = usersViewModel.foo;
if (future == null) {
return const Text('null');
}
switch (future.status) {
case FutureStatus.pending:
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(),
Text('loading'),
],
);
case FutureStatus.fulfilled:
final String items = future.result;
return Text(items);
}
return const Text('loading');
}),
這個 future
的 type 是 ObservableFuture
在操作它時就就跟我們在 FutureBuilder
裡操作 Snapshot
一樣,所以我們可以從他的 status
來得知這個非同步事件目前的狀態是什麼。
接下來就是開始搬移我們昨天fetch User的程式碼
final HttpService _httpService = HttpService();
@observable
ObservableFuture<ObservableList<Users>>? userList;
@action
Future fetchUserList() {
return userList = ObservableFuture(Future(() async {
final response = await _httpService.get('users');
final jsonStr = json.encode(response.data);
final ObservableList<Users> result =
ObservableList.of(usersFromJson(jsonStr));
return userList = ObservableFuture.value(result);
}));
}
這裡會發現一個很麻煩的事情就是我們在 ObservableFuture
裡在包一層 Future
然後才在Future
裡面的執行我們的真正的實作。
這樣子是為了讓 observer 才能正確的得知 ObservableFuture
的裡面的 Future
status真的有改變。
但當然還是有其他比較簡單的方式可以達到這個需求,接下來的範例會使用另外一種方法。
既然我們現在有了一些非同步事件,那我們來試著使用 MobX 的 reactions 來管理看看,首先我們先在MobX store裡宣告這三個 observable
來表示我們選擇到的id及User還有讀取狀態,然後兩個 @action
來改變這兩個observable
。
@observable
String? seletedUserid = '1';
@observable
ObservableFuture<Users>? seletedUser;
@observable
bool loading = false;
@action
void seletedUsesrid(String? userid) {
seletedUserid = userid;
}
@action
Future fetchSeledtedUser(String? userid) async {
runInAction(() {
loading = true;
});
final response = await _httpService.get('users/$userid');
final Users result = Users.fromJson(response.data);
runInAction(() {
seletedUser = ObservableFuture.value(result);
loading = false;
});
}
這邊的 runInAction
就是會在 async Action 中的另外一個scope 他會立即去變更 observable
,所以我就可以不用讀取 Future
狀態而是直接我們自己用另外一個值去替代。
接下來開始實作這功能的UI,我這裡將 DropdownButton
抽出來成為另一個單獨的 Widget
class CustomDropdownButton extends StatelessWidget {
CustomDropdownButton({Key? key, required this.usersViewModel})
: super(key: key);
final UsersViewModel usersViewModel;
@override
Widget build(BuildContext context) {
return Observer(builder: (_) {
return DropdownButton<String>(
value: usersViewModel.seletedUserid,
icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (String? newValue) {
usersViewModel.seletedUsesrid(newValue);
},
items: List.generate(10, (index) => (index + 1).toString())
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
});
}
}
這裡比較重要得參數就是 value
、 onChanged
、 items
value
就是這個 DropdownButton
所綁定的 state 所以這邊就直接綁定 usersViewModel.seletedUserid
然後 onChanged
就是綁定我們的 action
items
可以想像成select/option 裡的 option 所以我們就放入 List<DropdownMenuItem<String>>
裡面是1到10的選項。
然後將要根據這個 DropdownButton
變更的UI寫好:
Observer(
builder: (_) {
final future = usersViewModel.seletedUser;
if (future == null) {
return const Text('null');
}
switch (usersViewModel.loading) {
case true:
return const CircularProgressIndicator();
case false:
final Users items = future.result;
return Column(
children: [
Text(items.name!),
],
);
}
return Text('null');
},
),
接下來你會發現,你無論怎麼按都不會變更這個 Observer
的UI,因為我們的DropdownButton
onChange時只有觸發 seletedUsesrid
而沒有去做 fetch
這時就可以寫上我們的 Reactions
late List<ReactionDisposer> _disposers;
void setupReactions() {
_disposers = [
reaction<String?>((_) => seletedUserid, fetchSeledtedUser),
];
}
void dispose() {
for (var d in _disposers) {
d();
}
}
然後到 widget裡註冊
@override
void initState() {
usersViewModel.fetchUserList();
usersViewModel.fetchFoo();
super.initState();
usersViewModel.setupReactions();
}
@override
void dispose() {
usersViewModel.dispose();
super.dispose();
}
我們在 initState
時註冊我們的Reactions 然後在dispose時清除掉。
通常Reactions就是當我們監聽的對象,有所變化時我們須要執行的side effect都會放在這裡。
現在我們來看一下這段code reaction<String?>((_) => seletedUserid, fetchSeledtedUser)
,就是指seletedUserid
變化時要執行 fetchSeledtedUser
。
當然Reactions 還有其他方法可以使用,這裡就不多贅述了。
最後的成品如下:
今天的程式碼
https://github.com/zxc469469/flutter_rest_api_playground/tree/Day24
今天的內容稍嫌複雜了點,我自己對於MobX非同步的操作也不是很有把握說一定是best pratice,所以如果有錯的話還請幫我留言更正一下QQ
那接下來就是開始實作路由的功能了。