iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Modern Web

Flutter web 的奇妙冒險系列 第 24

Day 24 | 在flutter 中串接 restful api - MobX的非同步操作

那今天就來讓這個非同步資料透過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(),
      );
    });
  }
}

這裡比較重要得參數就是 valueonChangeditems

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://ithelp.ithome.com.tw/upload/images/20211007/20112906SQ3RfDaUq9.png


今天的程式碼

https://github.com/zxc469469/flutter_rest_api_playground/tree/Day24

今天的內容稍嫌複雜了點,我自己對於MobX非同步的操作也不是很有把握說一定是best pratice,所以如果有錯的話還請幫我留言更正一下QQ

那接下來就是開始實作路由的功能了。


上一篇
Day 23 | 在Flutter裡串接restful api - 我不使用HttpClient了 jojo
下一篇
Day 25 | Flutter 路由管理套件 - auto_route
系列文
Flutter web 的奇妙冒險30

尚未有邦友留言

立即登入留言