Flutterの備忘録
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に集中できるようにしてくれている
- 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で確認cocapodが無いと言われるのでbrewで入れる
[!] 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
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を動的に変更する場合は
- 状態を変更する
- buildメソッドを呼び出す
- 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を実行する際に利用
- ViewからActionが実行される
- Actionが呼び出されたら必要に応じてMiddleWareで処理挟む(MiddleWareは必須でない)
- Actionを元にReducerでStateを変更
- 変更したStateをStoreに登録
- 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に反映したい場合
など
- UIからアクションが発行される
- MiddleWareでアクションを元に別の値を生成する
- 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)
- Stateの
- 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'); })
- Providerで非同期な処理をしたい時に使用する
- 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