【Flutter】Googleの推奨するステート管理方法のProviderを導入してみた

#はじめに
G's Academy Lab10期の中村です。

今回はFlutterについて書こうと思います。
モバイルアプリケーション開発が初めてなので、ツッコミどころ満載かと思いますがぜひ読んでいただければと思います。
今回は課題で絶賛作成中のPFC管理のアプリケーションの一部を例に進めてみたいなと思います。
公式ドキュメントを参照していますが間違いなどはコメントいただけると幸いです。
それでは、本題に入ります。

#想定読者
自分のようなFlutter初心者の方でState管理をどうしようと悩んでいる方です。基本的な内容は抑えている前提で話を進めます。ゴリ押しで一応動くコードになったけど、どうやったらきれいで効率的になるか気になっている方も参考になるかと思います。

#導入に至った経緯
実際に現場ではアプリケーション自体が複雑になることが多いです。
そんな中、自分のアプリケーションでも「現場に入ったら」を意識して、システムのアーキテクチャの部分もしっかり勉強しようと思ったので導入してみました。

#State管理について
##モチベーション
そもそもどうしてステート管理を導入するかという話ですが、これは公式ドキュメントにわかりやすいgif画像があります。
![state-management-explainer-5495afe6c3d6162f145107fe45794583bc4f2b55be377c76a92ab210be74c033.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413235/6aca4a4b-a4b4-23fc-bb28-ccd5c19b4258.gif)
>As you explore Flutter, there comes a time when you need to share application state between screens, across your app. There are many approaches you can take, and many questions to think about.

引用:https://flutter.dev/docs/development/data-and-backend/state-mgmt/intro

簡単にまとめると、それぞれ個別のScreenで管理しているStateを共有したいという状況が、アプリケーションを作成していると起こります。その実現のためにステート管理を導入しよう、という流れになります。
`MyListItem`という子Widgetでのアクションが`MyCart`のWidgetで反映されています。このようなシチュエーションでProviderなどのステート管理が威力を発揮します。

##主なState管理方法
State管理の方法も公式ドキュメントにはいくつか紹介がされています。
 [Google I/O 2019](https://www.youtube.com/watch?v=d_m5csmrf7I)ではProviderを使うことが推奨されています。デファクト・スタンダードになってきているようです。
30分程度の動画なので一度見てみるといいかと思います。ちなみにGoogle I/O 2018ではBLoCを使うことが推奨されていたようです。

- Provider
- setState
- InheritedWidget & InheritedModel
- Redux
- Fish-Redux
- BLoC / Rx
- GetIt
- MobX

参考：https://flutter.dev/docs/development/data-and-backend/state-mgmt/options

### アプリの仕様
ここから実際にProviderの理解するべき概念をサンプルコードと一緒に紹介します。
今回はユーザーがPFC管理の設定画面で目標値を入力して記録ボタンを押すとその結果が別スクリーンにも反映されるようにProviderを利用します。
パッケージの導入の仕方は[ここを確認](https://pub.dev/packages/provider/install)してください。
<img width='200' alt="simulator_screenshot_BB2D5FA2-DCEA-4A5C-A28E-CC3ADE520023.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413235/fc5ec96d-725c-c760-325c-2d3f1a061641.png">　　<img width="200" alt="simulator_screenshot_2E99792E-C04D-47A6-84FF-FD9003422D25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413235/bbb3545e-a56a-979e-249b-00f83fd154da.png">
左の画像：目標値の入力画面
右の画像：ホーム画面目標値がそれぞれ、 `/〇〇g`の〇〇に入る。

##Providerで理解するべき3つの概念
大きく3つの概念を理解できればProviderを導入することができます。

- ChangeNotifier
- ChangeNotifierProvider
- Consumer

それぞれ見ていきましょう。
##ChangeNotifer
>ChangeNotifier is a simple class included in the Flutter SDK which provides change notification to its listeners. In other words, if something is a ChangeNotifier, you can subscribe to its changes. (It is a form of Observable, for those familiar with the term.)

軽くまとめると、ステートに変更が起きたときにそれをListenerに通知することができます。共有したいstateの内容をクラスのプロパティの形で保持します。

記録ボタンを押すと、以下のサンプルコードの`updateUserData`の処理が走ります。
`setUserData`の最終行の`notifyListeners()`を忘れると変更しているはずがその内容が通知されないため変更されないということになるので気をつけてください。

```:lib/models/user.dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:intl/intl.dart';
// Changenotifierをextendする
class UserModel extends ChangeNotifier {
  
  int aimCal = 0;
  int aimPro = 0;
  int aimFat = 0;
  int aimCar = 0;
  
  ... 

  //外部のAPIサーバー、UserController@updateにアクセス
  void updateUserData(aimCal, aimPro, aimFat, aimCar) async {
    final response = await http.put(baseURL + '/$userId',
        body: {'aim_cal': '$aimCal', 'aim_protain': '$aimPro', 'aim_fat': '$aimFat', 'aim_car': '$aimCar'},
        headers: {"Accept": "application/json"}
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      print(data['message']);
      setUserData(data);
    } else {
      print('Request failed with status: ${response.statusCode}.');
    }
  }

...
   // 3項演算子はApiの返り値がString型でエラーを吐いたので処理
  void setUserData(data) {
    userId = data['user']['id'];
    uid = data['user']['uid'];
    aimCal = data['user']['aim_cal'] is String ? int.parse(data['user']['aim_cal']): data['user']['aim_cal'];
    aimPro = data['user']['aim_protain'] is String ? int.parse(data['user']['aim_protain']): data['user']['aim_protain'] ;
    aimFat = data['user']['aim_fat'] is String ? int.parse(data['user']['aim_fat']): data['user']['aim_fat'];
    aimCar = data['user']['aim_car'] is String ? int.parse(data['user']['aim_car']) : data['user']['aim_car'];
    notifyListeners(); // これが変更を通知するために必要！！
  }
}
```


##ChangeNotifierProvider
>ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the provider package.

軽くまとめると、1つ目の概念のChangeNotifierでの変更を通知するために必要になります。ここで注意すべき点として、ChangeNotifierProviderの子Widgetでしか、変更を購読できないので適切な階層で設定してください。

```lib/main.dart
void main() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    runApp(
      // 今回はすべてのWidgetで変更を購読できるようになっています。
      ChangeNotifierProvider(
        create: (context) => UserModel(),
        child: MyApp(),
      ),
    );
}
```

##Consumer
これが実際に変更を購読する箇所で利用するものになります。これを利用することで、別スクリーンで変更が起きたときに`Consumer`を利用している箇所で変更内容が自動的に反映されます。

```:lib/screens/setting.dart
...一部抜粋
Consumer<UserModel>(
   builder: (context, user, _) {
     return ValueMeter(todayTotalValue: user.aimCal, labelText: '摂取カロリー', unitAndValue: 'kcal',);
   }
),
...
```
このように利用します。
注意点として、
`Consumer<モデル名>`の<モデル名>を忘れないように。
また、引数は`builder: (context, インスタンスの名前, ChildWidget)`
になります。
今回はmodelの内容が変更されると`Consumer`で`UserModel`のインスタンスの`aimCal`を購読しているので、変更後の値が自動的に表示されるようになります。

参考:https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple

#まとめ
いろいろ書きましたが、一行でProviderをまとめると次のようになるかなと思います。
**複数スクリーンで利用したいものをモデルにプロパティとして定義してあげて(ChangeNotifier)、プロパティの変更が通知されるようにし(ChangeNotifierProvider)、その通知結果を適応したい箇所で購読(Consumer)する。**
Providerはかなり直感的でわかりやすいState管理方法だと思います。公式ドキュメントにも利用の仕方がサンプルとともにわかりやすく説明されているので、Flutterは開発しやすいのかなと感じます。参考になれば幸いです。

#参考文献
https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple