🍹

Flutterの備忘録

2022/07/16に公開

Software DesignのFlutterの記事の備忘録

Flutterの特徴

  • 独自のレンダリングシステム
    • Skiaという描画ライブラリが組み込まれた独自のレンダリングシステムがある
    • このおかげでネイティブでの実装と比較しても遜色ないパフォーマンスを実現
  • 各プラットフォーム対応状況が進んでおり、1つのコードベースで複数のプラットフォームで動作する価値が高まっている
    • Android, iOS以外にもWeb, Windowsは安定版になっている
    • macOS, Linuxもベータ版として開発中
    • iOS/Androidエンジニア区別なく1つにまとまって少人数開発できる
    • プラットフォーム間の実装の差が生まれにくい
  • Dart言語
    • Googleが開発している言語
    • Java, JavaScriptと似ている
    • Null Safety
      • Dart 2.12で入った
      • 予期せぬNullへの参照をビルド前にチェックできる
      • 非Null許容型とNull許容型を明確に区別している
        • Null型の変数を非Null型の変数に入れる時はNullチェックしないと代入できない
        String value = ''; // 非Null型
        String? nullableValue = null;
        
        if (nullableValue == null) {
          return
        }
        
        value = nullableValue;
        
  • 他のクロスプラットフォーム(cordova, xamarin)に対する強み
    • ホットリロードによる圧倒的な生産性の高さ
    • DevTools
    • 初学者でも学習しやすい。公式Youtubeチャンネルもある
    • pub.devに高品質なパッケージがそろっている
    • iOS, Android固有の実装(ファイルシステム、加速度センサ、カメラなど)を呼び出せる
      • プラットフォーム固有の機能を呼び出すための機構を Plugin と呼んでいる
  • UIの構築するあらゆる要素は Widget として表現される
    • Widgetツリーという構造
      • Widgetの下層に別のWidget(子Widget)を渡し、さらの孫WidgetにもWidgetを渡していくという流れで、アプリ全体のレイアウトが管理されている
      • 厳密にはツリー構造の本体はWidgetから生成されるElementというオブジェクト
        • FlutterはElementを上手く隠蔽して開発者がWidgetに集中できるようにしてくれている
  • ホットリロードによる効率的な開発体験
    • ソースコード上で保存するだけで変更が反映される(いちいちコンパイルしなくて良い)

UIアーキテクチャー

Flutterを構成する要素としてFramework層がある
そのFramework層にあるWidgets、Material、Cupertioについて

  • Wigets
    • 基本となるシンプルなWidget
    • デザイナーが1からデザインしたブランド独自のデザインで利用
  • Material
    • Wigetsのラッパーでマテリアルデザインを強く意識したWidget
    • AppBar, ElevatedButtonなど
  • Cupertino
    • iOSが標準で提供するUIパーツをピクセル単位で再現したWidget
    • iOSの標準的なUIを再現することができる

Flutter SDKのインストール

こちらから最新版をDLてPathを通す
or homebrewで入れる

cd ~/development
unzip ~/Downloads/flutter_macos_arm64_3.0.5-stable.zip

or

brew install flutter

インストール完了したらFluter Dockerで現状確認
環境構築のために不足してくれる部分を指摘してくれる
現時点でAndroid SDKは対応していなかったり

🕙 22:52✗ flutter doctor
Running "flutter pub get" in flutter_tools...                       9.8s
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.0.5, on macOS 12.4 21F79 darwin-arm, locale ja-JP)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.dev/docs/get-started/install/macos#android-setup for detailed instructions).
      If the Android SDK has been installed to a custom location, please use
      `flutter config --android-sdk` to update to that location.

[✗] Xcode - develop for iOS and macOS
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
        sudo xcodebuild -runFirstLaunch
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage
        on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To install see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.
[✗] Chrome - develop for the web (Cannot find Chrome executable at /Applications/Google
    Chrome.app/Contents/MacOS/Google Chrome)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[!] Android Studio (not installed)
[✓] VS Code (version 1.68.1)
[✓] Connected device (1 available)
[✓] HTTP Host Availability

Androidのセットアップ

android studioもbrewで入れる

brew install android-studio

Android Studioを起動させてセットアップウィザードを進め、再度flutter doctorを試す

🕙 22:57❯ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[!] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.
[✓] Android Studio (version 2021.2)

android toolchainに注意マークが出てるので対応する

  • androidのcommand-line toolが入ってないから入れろと言っているので、こちらを参考にしてAndroidStudioのPreferenceから入れる
  • 上記対応後にライセンス許諾するコマンド実行する
    flutter doctor --android-licenses
    

Xcodeのインストール

  • AppStoreからXcodeをダウンロードしてインストール
  • Xcode付属のコマンドラインツールを最新版にしてライセンスに同意
    sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    sudo xcodebuild -runFirstLaunch
    sudo xcodebuild -license
    
  • flutter doctorで確認
    [!] Xcode - develop for iOS and macOS (Xcode 13.4.1)
      ✗ CocoaPods not installed.
          CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage
          on the Dart side.
          Without CocoaPods, plugins will not work on iOS or macOS.
          For more info, see https://flutter.dev/platform-plugins
    
    cocapodが無いと言われるのでbrewで入れる
    brew install cocoapods
    [✓] Xcode - develop for iOS and macOS (Xcode 13.4.1)
    

Widgetでのコンストラクタ内のconstの意義

constをコンストラクタの呼び出しコードに付与することで
コンパイル時に1つだけオブジェクトを準備して、何度同じステップが実行されてもオブジェクトが使い回される
という重要な仕組みのために付けている
例えば、再描画が走った際に最初にコンストラクタで作成されたテキストやスペーサーオブジェクトを毎回生成し直すことはしない
各オブジェクトが再描画の判断を個々に行っている
これにより最適な再描画処理が行えている(無駄な再計算が発生しない)
Widgetのコンストラクタ内でconst使わないとLintに怒られるぐらい根本的な設計思想がある

Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar:  AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              const Spacer(), // この辺
              const TextField(),
              const SizedBox(height: 32.0),
              ElevatedButton(
                onPressed: () {},
                child: const Text('保存')
              ),
              const Spacer()
            ]),
        ),
      ),
    );
  }

似た修飾子としてfinalがあるが、こちらは実行時に生成したオブジェクトを1度だけ代入できるというもの
constは実行前(コンパイル時)にオブジェクトを生成するという違いがあるので注意

Flutterの状態管理について

FlutterのUIは公式がある

UI = f(State)

UI: 最終的に画面に表示されるUI
f: buildメソッドによるWidgetの構築処理
State: buildメソッド内で参照される状態

UIはbuildメソッドの戻り値によって決定され、buildメソッドが状態を参照している

UIを動的に変更する場合は

  1. 状態を変更する
  2. buildメソッドを呼び出す
  3. buildメソッドが戻り値として再計算されたWidgetを返す

をしている
この一連のリビルドの流れのことを状態管理と言っている

宣言的なUI構築

buildメソッドのプログラムは
この場合のUIはこうである
と常に宣言している読み取ることができる
こうしたプログラミング手法のことは宣言的なUI構築と言われている

  • どんな経緯であったとしても、状態とbuildメソッドを確認すれば最終的なアウトプットは決定できる
  • 動的にUIを変更するためにすることが、状態の変更とリビルドだけとシンプル

StatefulWidgetの課題

状態管理のために標準で用意されている

  • 状態管理だけでなく、buildメソッドもStateクラスが担当するのでStateクラスが密結合になってしまう
  • 複数のWidget間で状態を共有するためにはGlobalKeyを利用したりしないといけないためスパゲッティコード化しやすい

InheritedWidget

StatefulWidgetの課題であった、状態を管理するWidgetと状態を利用するWidgetを分離する思想のWidget

  • メリット
    • InheritedWidgetの子孫であればどのWidgetからもアクセスできるので状態を共有できる
    • 下層のどのWidgetからもO(1)でアクセスできる
      • O(1): 扱うデータ量がどれだけ大きくなっても処理量は変わらないこと
    • アクセスのあったWidgetをキャッシュし、状態が変化した場合にリビルドを発生させる
  • デメリット
    • 状態を管理するコードとそれを利用するコードが分離して関連がわかりづらくなってしまう
    • InheritedWidgetを利用するためにボイラープレートコードを書く必要がある

providerパッケージ

InheritedWidgetの課題を解決するために作られたInheritedWidgetのラッパー

  • ボイラープレートコードの削減
  • InheritedWidgetへの共通したアクセス手法の提供
  • 開発ツールによる状態の可視性の向上

riverpodパッケージ

providerはInheritedWidgetのラッパーという前提上、Flutterフレームワーク自体の設計に従わないといけない
それ故の使いづらさを解決するために作られた

  • 同じ型の状態のオブジェクトを複数管理できる
  • BuildContextとは別にProviderRefを利用することでも状態オブジェクトにアクセスできる
  • テストのために状態オブジェクトの振る舞いを上書きできる
  • 仕組みとしてはWidgetツリーとは独立していて、状態オブジェクトの生成や破壊の挙動などriverpod独自の仕組みのため使い方注意

その他の状態管理パッケージ

List of state management approaches

flutterのテスト

テストの種類は主に3つ

  • Unitテスト: 各クラス・メソッドのテスト
  • Widgetテスト: Widget単位のテスト。Flutter特有のテスト
  • Integrationテスト: アプリのテスト

Widgetテスト

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:myfirstapp/main.dart';

void main() {
  // testWidgetsの第一引数はテストの概要、第二引数にコールバックでテストロジックを渡す(非同期関数にて)
  // コールバックに渡される引数はWidgetTester型のインスタンスで、これを使うことでWigetの操作ができるという仕組み
  testWidgets('スタート画面が表示される', (WidgetTester tester) async {
    await tester.binding
        .setSurfaceSize(const Size(400, 800)); // テスト時の画面サイズが縦長になるように調整
    await tester.pumpWidget(const MyApp()); // pumpWidget()でMyAppをレンダリングする

    final titleFinder = find.text('スライドパズル'); // MyAppに指定の文字を含むWidgetがあることを確認
    final buttonFinder = find.text('スタート'); // MyAppに指定の文字を含むWidgetがあることを確認

    // find.textだと対象のWidgetが存在しない場合や複数存在した場合に正しくテストできない
    // そのため特定したWidgetが期待した内容であるかを判定するためにfindsOneWidgetというMacherを使う
    expect(find.text('スライドパズル'),
        findsOneWidget); // MyAppに指定の文字を含むWidgetが「1つ」あることを確認
  }); // WidgetのテストにはtestWidgetsを使う

  testWidgets('スタートボタンをタップするとパズル画面が表示される', (WidgetTester tester) async {
    await tester.binding
        .setSurfaceSize(const Size(400, 800)); // テスト時の画面サイズが縦長になるように調整
    await tester.pumpWidget(const MyApp()); // pumpWidget()でMyAppをレンダリングする

    final buttonFinder = find.text('スタート'); // MyAppに指定の文字を含むWidgetがあることを確認
    await tester.tap(buttonFinder); // テスト時にボタンをタップする(tap非同期なのでawaitを必ずつけること)
    // await tester.longPress(find.byIcon(Icon.add)); // アイコン長押しの例
    // await tester.enterText(find.widgetWithText(TextField, 'hoge'), 'fuga'); // 指定したテキストのフォームに指定した文字を入力
    await tester.pumpAndSettle(); // テスト時にボタンをタップ後に画面を更新、表示されるのを待つためのメソッド

    expect(find.text('1'), findsOneWidget); // MyAppに指定の文字を含むWidgetが「1つ」あることを確認
    expect(
        find.text('シャッフル'), findsOneWidget); // MyAppに指定の文字を含むWidgetが「1つ」あることを確認
  }); // WidgetのテストにはtestWidgetsを使う
}

flutter testコマンドでテスト実行

flutter test
00:01 +0: スタート画面が表示される
00:02 +0: スタート画面が表示される
00:02 +1: スタート画面が表示される
00:02 +1: All tests passed!

DevTools

アプリをデバッグ起動している状態で以下のコマンドをターミナルから実行する

flutter run

以下の様なURLをクリックするよう案内される

The Flutter DevTools debugger and profiler on iPhone SE (3rd generation) is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:57013/XOctQCj94pc=/

ブラウザで該当のURLをクリックすると以下のようなデバッグツールが表示される
Flutter InspectorタブではWidget Treeが確認できる
該当のWidgetをクリックして、カーソルを合わせるとWigetが持つプロパティ一覧が閲覧できる
現在保存されている値を確認したり、親子関係を把握するなどに有用

多分1週間くらいでできるFlutter入門の備忘録

Software DesignのFlutterの記事の備忘録

基本ファイルについて

flutter create helloapp で生成されるディレクトリ配下のファイルで主なものを紹介

  • helloapp.iml
    • AndroidStudioなどで利用する設定ファイル
  • android
    • Androidアプリを作る際に必要な設定やDartで対応できないネイティブコードをKotolinで作る時に利用する
  • ios
    • iOSアプリを作る際に必要な設定やDartで対応できないネイティブコードをswiftで作る時に利用する
  • lib
    • 実際にプログラムを書いていく際こちらにファイルを追加していく
    • 配下のmain.dartはFlutterアプリのエントリーポイントのファイル
  • pubspec.yaml
    • プロジェクト名やバージョン情報、利用するライブラリの管理
    • RubyでいうGemfileのようなもの
  • pubspec.lock
    • pubspecでプラグインをインストールした際に生成されるファイル
  • web
    • webアプリを作成するときの設定などを用意する場所

main.dart

main()はdartで一番最初に呼ばれるエントリーポイントですべてはここからはじまる
runnapp()でFlutterが実行される
MyAppはUIを生成するためのWidgetを継承したクラスで、runAppが実行されて初めてFlutterとして処理が開始される

void main() {
  runApp(const MyApp());
}

StatefulWidget と StatelessWidget

StatelessWidgetは値を保持しないWidget

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

   // dartのアノテート。この場合親クラスにあるbuildをオーバーライドしていることを意味する
  Widget build(BuildContext context) { // BuildContextはこのメソッドを呼んだ親クラスのElementになる
    return MaterialApp( // MaterialAppはFlutterアプリの全体を管理するWidget。
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'), // homeでFlutterで最初に呼び出されるアプリを指定する
    );
  }
}

StatefulWidget

値を保持することが必要な時に利用するWidget
今回の場合はカウンターの値を保持
StatefullWidgetを使用する時は、StatefulWidgetを継承したクラスStateを継承したクラス をペアで使う必要がある

// MyHomePageがStatefulWidgetを継承し
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  // そのStateを_MyHomePageStateが保持している
  // createStateメソッドは必ずState<T>クラスを戻り値として返す必要がある
  // 今回の場合_MyHomePageStateクラスを返している
  // 動きとしてはStatefulWidgetがビルドされたタイミングで実行されStateクラスを呼び出す
  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> { // MyHomePageを継承している理由は、MyHomePageクラスで定義している変数へアクセスするため
  int _counter = 0;

  void _incrementCounter() {
    // setStateの引数にコールバック関数でstateの変更処理を渡すことで
    // stateの更新と描写の変更を自動でやってくる
    setState(() {
      _counter++;
    });
  }

  // 長いがやっていることはUIを構成している各Widgetのネスト
  
  Widget build(BuildContext context) { // Statelessと同じ親クラスのElementを受けっとている
    return Scaffold( // Scaffoldは足場の意。画面に表示される内容の足場を生成
      appBar: AppBar( // ScaffoldのappBarプロパティは画面上部のヘッダー部に関する責務を担っている。今回はそこにAppBarというWidgetを渡して上げている
        title: Text(widget.title), // MyHomePageを継承しているからwidget.titleでMyHomePageのタイトルプロパティを参照できる
      ),
      body: Center( // bodyは画面のメインになる部分の描画を担当。そこにCenterというWidgetを渡している。Centerはchildに渡されたWidgetを中央に表示するのに使う
        child: Column( // Column Widgetは縦方向にWidgetを連続させて表示する時に使うもの。横方向に並べたい時はRow Widgetを使う
          mainAxisAlignment: MainAxisAlignment.center, // 子のWidgetを並べる際の軸の設定。この場合は真ん中から並べる設定
          children: <Widget>[ // Column内に並べるWidgetを配列で突っ込んでいる。今回は文字列を表示させるTextWidgetとカウンターを表示させるTextWidgetの2つ
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton( // floatingActionButtonはデフォルトでは右下に表示されるボタン。FloatingActionButtonクラスと合わせて使うのが基本
        onPressed: _incrementCounter, // ボタンが押された時に実行される
        tooltip: 'Increment', // ボタンが押された時にどのような動作をするか説明する文章(アプリの動作には関係ない)
        child: const Icon(Icons.add), // ボタンに表示する文字やアイコンの設定
      ),
    );
  }
}

Flutterでの画面遷移

Navigator.push: 画面遷移
Navigator.pop: 1つ前に戻る

// NewTodoPageへ画面遷移(オーバーレイ)
// NewTodoPageから値を返してもらう処理も書ける
Map<String, String> todo = await Navigator.push(
  context, // 使い方は現在のWidgetのContextと
  MaterialPageRoute(
    builder: (context) => NewTodoPage()
  ), // MaterialPageRouteを第2引数で渡す。
);

// NewTopPageから元のWidgetに戻る(値と共に)
Navigator.pop(context, {
  'title': _titleController.text,
  'content': _contentController.text,
});

画面移動(遷移)の仕組みから説明すると、画面は移動(遷移)をおこなうと、前の画面の上に1つ画面がかぶさる(オーバーレイ)状態になります。 これにより積み重なったウィジェットはユーザの画面移動の履歴となります。

Widgetの生成と破棄を繰り返すことでも画面遷移は表現することができるが、画面の再生コストが高いためこの様なオーバーレイで表現している

よく使いそうなWiget達

画面下部のナビゲーションバーにアイコンとラベル

BottomNavigationBarItem(icon: Icon(Icons.push_pin), label: 'To'),

onPressedとonTapの違い

  • onPressed: 簡素なクリックイベント実装(FlatButton)
  • onTap: リッチなクリックイベント(GestureDetectorを利用するのでスワイプなどのリッチに)

SnackBar

SnackBarと似ている機能にToastがある
Toastはあまり出番ないらしい
アクションを含むことができなかったり、画面からスワイプすることができないという特徴がある
SnackBarが表示できない場合にToast使いましょうというガイドラインがある

ScaffoldMessenger.of(context).showSnackBar(SnackBar(
  content: Text("状態を更新しました"),
));

ExpandedとSizeBox

  • ExpandedWidget
    ExpandedWidgetは子Widgetを表示領域いっぱいに表示するためのWidget
    通常は無制限の縦or横方向の表示になる。
    しかし、Expandedを使うことで縦or横方向の上限いっぱいまでという制約を付与することができる。
  • SizedBox
    Expanedとは対象的にwidthとheigtで厳密にオブジェクトサイズを指定した使い方をする

状態管理:Redux

Reduxを使うメリット
StatefulWidgetの中で管理していたState はWidgetに閉じたState管理。
一方、ReduxはRedux上のState空間で一括管理できる

Reduxの基本用語

  • State: Stateを保持している空間
  • Action: Stateを変更するための発火イベント
  • Reducer: Actionイベントを受け取ってStateを変更するロジック
  • Store: Stateの表示はStoreから参照するという使い方。Storeはシングルトンである必要がある
  • MiddleWare: 非同期な処理を必要とする場合や、別のActionを実行する際に利用

  1. ViewからActionが実行される
  2. Actionが呼び出されたら必要に応じてMiddleWareで処理挟む(MiddleWareは必須でない)
  3. Actionを元にReducerでStateを変更
  4. 変更したStateをStoreに登録
  5. Storeの内容をViewで表示

Store

// Stateは非同期な関数として定義する
// 戻り値はFutureとして、ジェネリクスはreduxのライブラリーのStateクラスを使う
// StateクラスのジェネリクスにState全体を取りまとめたAppStateを使う
Future<Store<AppState>> createStore() async {
  return Store(
    appReducer,
    initialState: AppState.initial(),
    middleware: [
      TodoMiddleware(),
      BottomNavigatorMiddleware(),
    ],
  );
}

State

ReduxでStateはimmutableである必要がある


class AppState {
  final TodoState todoState;

  AppState({
     this.todoState,
  });

  // factory修飾子のことをfactoryコンストラクタと呼ぶ
  // 自動的にインスタンスが生成されなくなる(以下のように明示的に初期化処理を書かないといけなくなる)
  factory AppState.initial() {
    return AppState(
      todoState: TodoState.initial(),
    );
  }

  AppState copyWith(TodoState todoState, BottomNavigationState bottomNavigationState) {
    return AppState(
      todoState: todoState ?? this.todoState,
    );
  }
}

Reducer

// combineReducers: 引数で渡された配列に入っている各Reducerを一つにまとめて処理してくれる
final todoReducer = combineReducers<TodoState>([
  // TypeReducerを使うことで発行されたアクションとそれに対応して実行されるReducerを設定できる
  // もしTypeReducerを使わない場合はActionに応じたif文の分岐が発生してコードが複雑になるのでおとなしくTypedReducerを使う
  TypedReducer<TodoState, AddTodoAction>(_addTodoReducer),
  TypedReducer<TodoState, SetSelectTodosAction>(_setSelectTodos),
]);

TodoState _addTodoReducer(TodoState state, AddTodoAction action) {
  List<TodoContent> todos = state.todos;

  todos.add(TodoContent(title: action.title, content: action.content));

  return state.copyWith(todos: todos);
}

TodoState _setSelectTodos(TodoState state, SetSelectTodosAction action) {
  return state.copyWith(selectedTodos: action.selectTodos);
}

MiddleWare

MidduleWareはReduxを使う上で必須ではないが複雑な処理を行う際に有用
AのActionの結果を元にBのActionを実行したい場合
ネットワーク上や端末のストレージなどから非同期でデータを読み取ってStateに反映したい場合
など

  1. UIからアクションが発行される
  2. MiddleWareでアクションを元に別の値を生成する
  3. MiddleWareの中で別のアクションを発行して2で作った値をStateに設定する

StoreProvider

// main.dartの中


Widget build(BuildContext context) {
  // mainの中で生成したstoreを子Widgetから使用できるように伝播するためStoreProviderを使う
  // どこかでStateの更新が起きた際には別のWidgetでも必要に応じてstateが更新され画面に反映されるようになる
  return StoreProvider(
      store: store,
      child: MaterialApp(
        title: 'Todo App',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: TodoPage(
          title: 'Todo App',
        ),
      ));
}

StoreProvider.ofメソッドを使って親Widgetのstoreを取得することができる
そのためにStoreProvider.ofの引数には親Widgetのエレメントがあるcontextを渡してあげる必要がある

// todo_page.dartの中

Widget build(BuildContext context) {
  final store = StoreProvider.of<AppState>(context);

useSelector

useXXX系の便利なメソッドはHookWidgetを継承することで利用可能になる
HookWidgetはReactHookを参考にFlutterで利用できるようにしたもので、StatefulWidgetを使うよりWidget内でのState管理を簡素化できる


Widget build(BuildContext context) {
  final navigator = useSelector<AppState, int>((state) => state.bottomNavigatorState.navigation);

useSelectorは、初期表示Actionがdispatchされた際に実行されるコールバック
初回やAction実行前後で対象のstateが変更されている場合にコールバックを実行して特定のstateを取得して更新してくれる

Riverpod

Reduxと同じ状態管理ライブラリー
複数のProviderがあるので、やりたいことがどのProviderで実現可能かを理解しておくことが重要

  • Provider
    • 最も基本的なProvider
    • Stateの読み取り専用
      final hoge = Provider<String>((ref) => "HOGE");
      print(hoge)
      
  • StateProvider
    • Stateの読み・書きができる
      final hoge = StateProvider<int>((ref) => 0);
      hoge.state++;
      print(hoge)
      
  • StateNotifierProvider
    • StateNotifierのクラスを使ってstateで複雑な操作をするときに利用する
    • UIなどからProviderの操作をStateNotifierに用意されたメソッドで使用できる
      class Hoge extends StateNotifier<int> {
        Hoge(int initHoge): super(initHoge);
        void up() {
          state++;
        }
      }
      
      final upProvider = StateNotifierProvider<Hoge>((ref) { return Hoge(0) })
      context.read(upProvider).up();
      
  • FutureProvider
    • Providerで非同期な処理をしたい時に使用する
      final hoge = FutureProvider<String>((ref) { return Future.delayed(Duration(seconds: 3), () => 'hogehoge'); })
      
  • StreamProvider
    • Streamを作成して最新の状態の値を返す
    • 戻り値がStreamという点を除いて使い方や動作はFutureProviderと同じ

main.dartでの設定例

void main() async {
  // ProviderScopeをアプリのルートに設定することで、この下にあるWidgetでProviderを使うことができるようになる
  runApp(ProviderScope(child: MyApp()));
}

StateNotifierProviderの利用例

// ①Todo配列を管理するためのStateNotifier
class Todos extends StateNotifier<List<TodoContent>> {
  // StateNotifierを継承した場合はコンストラクタでStateの初期値を受け取って親クラスに渡すこと
  Todos(List<TodoContent> initTodos) : super(initTodos ?? []);

  void add({required String title, required String content}) {
    state = [...state, TodoContent(title: title, content: content)];
  }

  void update() {
    state = state;
  }
}

// ② ①で作ったStateNotifierを使って生成したProvider
final todosProvider = StateNotifierProvider<Todos, List<TodoContent>>((ref) {
  return Todos([]);
});

// ③ ②で作ったTodosのProviderとBottomNavigatorBarのProviderを使うSelectTodosのProvider
final selectTodosProvider = Provider<List<TodoContent>>((ref) {
  // refは他のProviderのstateの更新を監視したり、特定のProviderがまだ使われているか、特定のProviderがdisposeされた際の動作を実装することができる
  // ref.watchで渡されたProviderのstateを監視する
  // watchメソッドで取得したStateはそのStateが変更された時に再作成されるので変更も読み取れる
  // readメソッドというものも存在し、そのタイミングでのstateのみを読み取り、更新されないので注意
  final navigator = ref.watch(bottomNavigatorNumProvider);
  final todos = ref.watch(todosProvider);

  switch (navigator.state) {
    case 0:
      return todos;
    case 1:
      return todos.where((todo) => todo.isTo()).toList();
    case 2:
      return todos.where((todo) => todo.isDoing()).toList();
    case 3:
      return todos.where((todo) => todo.isDone()).toList();
    default:
      return todos;
  }
});

useProvider


Widget build(BuildContext context) {
  // useProviderを使うことでProviderへアクセスできる
  // useProviderはStateControllerオブジェクトを返す。このStateControllerオブジェクトは、stateの変更を監視して変更があれば自動で更新する
  final navigator = useProvider(bottomNavigatorNumProvider);

Discussion