flutter_bloc公式ドキュメント→ https://pub.dev/packages/flutter_bloc
Contents
flutter_blocとは
✅ blocとcubitをFlutterに簡単に統合できるWidget
✅ blocパッケージで動作されるように構成されている
✅ flutter_blocパッケージによってexportされた全てのWidgetはCubitインスタンスとBlocインスタンス両方を統合する
✅ Blocは起こりうる状態変化のタイミングを規制し、状態を変化させる単一の方法を実行することで状態変化を予測可能にしている
Blocを採用する理由
bloc公式ドキュメント→ https://bloclibrary.dev/#/
✅ Blocはpresentation(UI, Widget, BLoC)からbusiness logic(UseCaseであるビジネスロジックに当たる部分)を簡単に分離できるため、テストが容易で再利用できるコードを高速に作成できる
✅ 本番品質のアプリケーションを構築する場合、状態管理が重要になる
⇨開発者は以下のことができる環境を作ることが重要
- アプリケーションがどのような状態にあるかをいつでも把握することができる
- アプリケーションが適切に動作、応答していることを確認するのに、全てケースを簡単にテストすることができる
- データドリブンな意思決定が行えるように、アプリ内の全てのユーザーインタラクションを記録できる
- 可能な限り効率的に動作し、アプリケーション内外の両方でコンポーネントを再利用できる
- 全ての開発者が同じパターン、規則に従って単一のコードベース内でシームレスに作業できる
- 高速かつ反応性の高いアプリ開発ができる
✅ Blocは上記のようなニーズを満たすように設計されており、次の3つのコアバリューを核に設計されている
Simple: 理解しやすく、様々なスキルレベルの開発者が使用可能
Powerful: より小さなコンポーネントで構成することで非常に複雑なアプリケーション作成を支援する
Testable: アプリケーションのあらゆる側面を簡単にテストできるため
Core Concepts (package:bloc)
blocの概念を理解するためにカウントアップするアプリを例に見てきます。
それに伴い、必要な知識も補足に記述していきます。
package:blocの導入
Installation
blocを導入する前に依存関係にあるパッケージのインストールが必要なので、
必ずbloc公式ドキュメントで確認します。
1 2 3 |
// <pubspec.yaml> dependencies: bloc: ^7.0.0 |
Import
インストールが完了したら、dartファイルにインポートしてきます。
1 2 |
// <.dart> import 'package:bloc/bloc.dart'; |
Streams(事前知識)
✅ 連続性のある非同期データを生成するもの
✅ パイプに流れる水で例えると、”水は非同期データ”で”パイプがSteam”
👇 一番簡単なStreamを返す関数を見てみる。numbersリストがtoAdd関数に入り、Stream Controllerを通過してStreamとして返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import 'dart:async'; main() { final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; toAdd(numbers).listen((val) { print(val); // 45 }); } Stream<int> toAdd(List<int> numbers) { int num = 0; final _controller = StreamController<int>(); numbers.forEach((n) { num += n; }); _controller.sink.add(num); return _controller.stream; } |
👇上記のコードをasync*, yieldで作ってみます
✅ Dartでは”async*“(asyncジェネレータ関数)を記述することでStreamを生成することができる
✅ asyncの戻り値→Future、async*の戻り値→Streamの違いに注意
✅ yieldはasync*関数のみで使える、通常の関数におけるreturnの代わりであり、Streamを返す
countStream関数でasync*, yieldを使い1,2,3…のStreamを生成し、sumStream関数で生成したStreamを受け取り全ての値を合計したあと、その計算結果をsumとして返します。streamとして受け取った値をイテレートする場合は非同期処理にする必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 1,2,3..Streamを生成 Stream<int> countStream(int max) async* { for(int i = 0; int < max; i++) { yield i; } } // 非同期的にStreamを受け取り合計値を返す Future<int> sumStream(Stream<int> stream) async { int sum = 0; await for(int value in stream) { sum += value; }; return sum; } // 0-9の値を合計して出力 void main() async { /// Initialize a stream of integers 0-9 Stream<int>stream = countStream(10); /// Compute the sum of the stream of integers int sum = await sumStream(stream); /// Print the sum print(sum); } |
ここまででStreamsがどのように働くか理解できたと思います。
次にblocパッケージのコアコンポーネントである”Cubit”について学んでいきます。
Cubit
✅ Cubit: BlocBaseを継承するクラスであり、任意の型の状態を管理するために使われる
✅ 状態変化をトリガーに呼び出される関数を作る
✅ StateはCubitの出力であり、アプリケーションの状態一部を表す
✅ UIコンポーネントは状態を通知され、現在の状態に基づいてコンポーネントの一部を再描画できる
Creating a Cubit
👇 Cubitは以下のように生成される
✅ Cubitを作成するときは、Cubitが管理する状態の型(str, int,…)を定義する必要がある
✅ 複雑な状態管理を必要とする場合、型はプリミティブ型(int, bool,…)以外にもクラスも使用できる
✅ 初期状態の指定はsuper()の引数に渡し、呼び出すことで実行することができる
✅ 上記のようにCubitを生成することで、様々な状態でインスタンス化することができる
1 2 3 4 5 6 7 8 |
// Cubitの生成 class CounterCubit extends Cubit<int> { CounterCubit(int initialState) : super(initialState); } // CounterCubitをインスタンス化 final cubitA = CounterCubit(0) // state starts at 0 final cubitB = CounterCubit(10) // state starts at 10 |
State Changes
✅ Cubitはemitを介して新しい状態を出力する機能が備わっている
✅ “emit“はCubit内でのみ使用可能な、保護されたメソッド
✅ CounterCubitはincrement関数をパブリックメソッドとして公開しており、このメソッドは外部から呼び出してCounterCubitに状態をインクリメントするように通知できる
✅ increment関数が呼び出されると、状態ゲッターを介してCubitの現在の状態にアクセスし、1を足した新しい状態を発行する
1 2 3 4 5 |
class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() => emit(state + 1); } |
Using a Cubit
◆basic usage
✅ increment関数をを呼び出し状態変更トリガーすると、Cubit状態を0→1にする
✅ Cubitのclose()関数を呼び出すことで、内部状態のストリームを閉じる
1 2 3 4 5 6 7 8 |
// Basic Usage void main() { final cubit = CounterCubit(); print(cubit.state); // 0 cubit.increment(); print(cubit.state); // 1 cubit.close(); } |
◆Stream Usage
✅ Cubitはリアルタイムに状態更新を受信できるようにStreamを備えている
✅ CounterCubitをサブスクライブし、状態が更新される度にprint関数を呼び出す
1 2 3 4 5 6 7 8 9 10 11 |
// Stream Usage Future<void> main() async { final cubit = CounterCubit(); // インスタンス化 final subscription = cubit.stream.listen(print); // 1 2 3 cubit.increment(); cubit.increment(); cubit.increment(); await Future.delayed(Duration.zero); //awaitによる値が確定するまで待つ subscription.cancel(); cubit.close(); } |
Observing a Cubit
✅ increment関数より状態が更新されると、onChangeをオーバーライドすることでCubitの変更を監視することができる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() => emit(state + 1); @override void onChange(Change<int> change) { // Cubitの状態が更新されるとonChangeが走る print(change); // Change { currentState: 0, nextState: 1 } super.onChange(change); } } void main() { CounterCubit() ..increment() ..close(); } |
◆BlocObserver
✅ 状態管理すべきCubitが複数ある場合、各々の状態変更に対して処理を行いたい場合に使う
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() => emit(state + 1); } class SimpleBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('${bloc.runtimeType} $change'); // CounterCubit Change { currentState: 0, nextState: 1 } } } void main() { Bloc.observer = SimpleBlocObserver(); CounterCubit() ..increment() ..close(); } |
Error Handling
✅ Cubitにはエラーが発生したことを示す”onError”メソッドが用意されている
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() => emit(state + 1); } class SimpleBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('${bloc.runtimeType} $change'); // CounterCubit Change { currentState: 0, nextState: 1 } } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); print('${bloc.runtimeType} $error $stackTrace'); } } void main() { Bloc.observer = SimpleBlocObserver(); CounterCubit() ..increment() ..close(); } |
Bloc
✅ BLoc: Business Logic Component
✅ Cubitのようにfunctionではなく、”event“に依存して状態変化をトリガーする
✅ Cubit同様、BlocBaseを拡張する→パブリックAPIを備えている、状態ゲッターを介していつでもブロックの状態にアクセスできる
✅ BlocはBloc内の関数を呼び出し、新しい状態を直接発行(emit)するのではなく、イベントを受け取り、受信イベントを発信状態に変換する
Creating a Bloc
✅ Blocの作成方法はCubitに似ているが、管理する状態を定義するのではなくBlocが処理できるeventを定義する
✅ ”event”はボタンプッシュやページロードなどのユーザーインタラクションへの応答として追加され、Blocへ入力される
✅ Cubit同様、superを介して初期状態をsuperクラスに渡すことで指定する必要がある
✅ Blocは新しい状態を直接更新せず、必ずイベントハンドラー内の受信イベントに応答して状態変化を出力する必要がある
1 2 3 4 5 6 7 |
abstract class CounterEvent() {} class Increment extends CounterEvent() {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0); } |
State Changes
✅ Cubitのように関数で状態更新するのではなく、on<Event>APIを介してイベントハンドラーする
✅ イベントハンドラーは受信したイベントを発信できる状態に変換する役割を担う
1 2 3 4 5 6 7 8 9 10 |
abstract class CounterEvent() {} class Increment extends CounterEvent() {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0); on<Increment>((event, emit) { emit(state + 1); }) } |
Using a Bloc
✅ incrementイベントを追加して状態変化をトリガーする
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/// The events which `CounterBloc` will react to. abstract class CounterEvent {} /// Notifies bloc to increment state. class Increment extends CounterEvent {} /// A `CounterBloc` which handles converting `CounterEvent`s into `int`s. class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((event, emit) => emit(state + 1)); } } Future<void> main() async { final bloc = CounterBloc(); print(bloc.state); // 0 bloc.add(Increment()); await Future.delayed(Duration.zero); print(bloc.state); // 1 bloc.close(); } |
✅ Cubitと同様Blocは特殊なStream型であり、Blocをサブスクライブして状態をリアルタイムで更新することができる
1 2 3 4 5 6 7 8 9 |
Future<void> main() async { final bloc = CounterBloc(); final subscription = bloc.stream.listen(print); bloc.add(Increment()); bloc.add(Increment()); await Future.delayed(Duration.zero); await subscription.cancel(); bloc.close(); } |
Observing a Bloc
✅ BlocはBlocBaseを継承しているため、”onChange“を使うことで状態変化をobserveすることができる
✅ Cubitがfunction駆動型に対して、Blocはイベント駆動型であるため、”onTransition“を呼び出すことで状態変化をトリガーした要因に関する情報も取得することができる
✅ 状態変化はTransitionといい、現在の状態、イベント、次の状態を含む
✅ Cubit同様、CounterBlocクラスでoverrideする方法と、BlocObserverを継承したクラスに記述する方法がある
✅ Blocインスタンスのユニークな機能として、新しいイベントがBlocに追加される度に呼び出せる”onEvent“をオーバーライドできる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((event, emit) => emit(state + 1)); } } class SimpleBlocObserver extends BlocObserver { @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); print('${bloc.runtimeType} $event'); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('${bloc.runtimeType} $change'); } @override onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print('${bloc.runtimeType} $transition'); } @override onError(BlocBase bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); print('${bloc.runtimeType} $error $stackTrace'); } } void main() { Bloc.observer = SimpleBlocObserver(); CounterBloc() ..add(Increment()) ..close(); } |
Cubit vs. Bloc 使いわけ
Cubit Advantages
✅ Simplicity : Cubitの最大の利点は『単純さ』
⇨状態管理するCubitを継承したクラスと、状態を変更するためのパブリックメソッドを定義するでだけで済み、理解が容易で関連するコード量が少ない
⇔一方でBlocは状態管理するBlocを継承したクラス、Eventクラス、EventHandlerの実装を定義する必要がある
Bloc Advantages
✅ Traceability : Blocの最大の利点は『状態変化のシーケンス性(連続性)』
⇨ある状態変化をトリガーに引き起こされた別の状態変化など、遷移の正確な原因を把握することができる
⇨状態変化を引き起こすイベントを網羅的に捉えるために、イベントドリブンなアプローチを使うことの重要性は大きい(特にアプリの重要な機能に関わる状態 ex. Authentication)
✅ Advanced Event Transformations :
⇨”buffer”, “debounceTime”, “throttle”などのリアクティブ演算子を利用する必要がある時、Blocには着信イベントフローを制御、変換できるイベントシンクが備わっている
⇨EventTransformerを実装することで、Blocでの着信イベントの処理方法を変更することができる
1 2 3 4 5 |
<meta charset="utf-8">// <pubspec.yaml> dependencies: bloc: ^7.0.0 <meta charset="utf-8">// <.dart> import 'package:rxdart/rxdart.dart'; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { EventTransformer<T> debounce<T>(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } CounterBloc() : super(0) { on<Increment>( (event, emit) => emit(state + 1), /// Apply the custom `EventTransformer` to the `EventHandler`. transformer: debounce(Duration(milliseconds: 300)), ); } } |
Core Concepts (package:flutter_bloc)
package:flutter_blocの導入
Installation
flutter_blocを導入する前に依存関係にあるパッケージのインストールが必要なので、
必ずflutter_bloc公式ドキュメントで確認します。
1 2 3 |
// <pubspec.yaml> dependencies: flutter_bloc: ^7.3.0 |
Import
インストールが完了したら、dartファイルにインポートしてきます。
1 2 |
<.dart> import 'package:flutter_bloc/flutter_bloc.dart'; |
Bloc Widgets
BlocBuilder
✅ Blocとbuilder関数を必要とするFlutterウィジェット
✅ 新しい状態に応じてウィジェットをビルドする
✅ builder関数は何度も呼ばれる可能性があり、状態に応じてウィジェットを返す純粋な関数
※ナビゲーション、ダイアログ表示など状態変化に応じて”実行”したい場合はBlocListenerを参照👇
✅ blocパラメータを省略するとblocProviderと現在のBuildContextを使って自動的検索を実行する
⇨単一のウィジェットにスコープされ、親BlocProviderやBuildContextを介してアクセスできないBlocにする場合は指定する
1 2 3 4 5 6 |
BlocBuilder<BlocA, BlocAstate>( bloc: blocA, builder: (context, state) { // return widget here based in BlocA's state } ) |
✅ buildWhen: builder関数が呼ばれるタイミングを制御したい場合に使用
✅ 前と現在とbloc状態を取得しbool値で返す
⇨bool値がTrueの場合は、状態と一緒にbuilderが呼ばれリビルドされる、Falseの場合はリビルドされない
1 2 3 4 5 6 7 8 9 |
BlocBuilder<BlocA, BlocAstate>( buildWhen: (previousState, state) { // return true/false to determine whether or not // to rebuild the widget with state } builder: (context, state) { // return widget here based in BlocA's state } ) |
BlocProvider
✅ BlocProvider.of<T>(context)を介して子ウィジェットにblocを渡す
✅ 依存性注入(DI:Dependency Injection)ウィジェットとして働くため、blocの単一のインスタンスをサブツリー内の複数のウィジェットに提供できる
✅ BlocProviderはblocがBlocProvider.of<BlocA>(context)を介して検索されるとcreateが実行されるため、blocを遅延して生成する(blocは自動的にクローズ処理される)
⇨この動作をoverrideしてcreateをすぐ実行するには”lazy: false“に設定する
1 2 3 4 5 |
BlocProvider( lazy: false, create: (BuildContext context) => BlocA(), child: ChildA(), ); |
✅ ウィジェットツリー内の新しい部分に既存のBlocを使えるようにすることができる
✅ 既存のBlocを新しいルートで使用する必要がある場合に最も使われる
✅ BlocProviderはBlocをcreateしないため自動的にはクローズしない
1 2 3 4 |
BlocPriovider.value( value: BlocProvider.of<BlocA>(context), child: ScreenA(), ); |
✅ Blocを受け取る方法は以下の2つがある
1 2 3 4 5 |
//with extensions context.read<BlocA>(); // without extensions BlocProvider.of<BlocA>(context); |
MultiBlocProvider
✅ 複数のBlocProviderを1つにマージすることで可読性向上させ、複雑な構造を回避できる
1 2 3 4 5 6 7 8 9 10 11 |
//❌ BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), child: BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), child: BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), child: ChildA(), ) ) ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// ⭕️ MultiProvider( providers: [ BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), ), BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), ), BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), ), ], child: ChildA(); ) |
BlocListener
✅ BlocWidgetListenerとBlocを受け取り、Blocの状態変化に応じてlistener呼び出す
✅ ナビゲージョン、スナックバーの表示、ダイアログの表示などの状態変化を起こす必要がある機能に使われる
✅ listenerは初期状態を除いた状態変化ごとに一度だけ呼び出されるvoid関数
✅ blocを指定するとBlocProviderおよびBuildContextを介してアクセスできないBlocにアクセスできる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// non setting of bloc BlocListener<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, child: Container(), ) // setting of bloc that is not accessible via BlocProvider and the current BuildContext BlocListener<BlocA, BlocAState>( bloc: blocA, listener: (context, state) { // do stuff here based on BlocA's state }, child: Container() ) |
✅ listenWhen: ここに記載される条件がtureで返される時、listenerが走る
1 2 3 4 5 6 7 8 9 10 |
BlocListener<BlocA, BlocAState>( listenWhen: (previousState, state) { // return true/false to determine whether or not // to call listener with state }, listener: (context, state) { // do stuff here based on BlocA's state }, child: Container(), ) |
MultiBlocListener
MultiBlocProviderと同様
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
MultiBlocListener( listeners: [ BlocListener<BlocA, BlocAState>( listener: (context, state) {}, ), BlocListener<BlocB, BlocBState>( listener: (context, state) {}, ), BlocListener<BlocC, BlocCState>( listener: (context, state) {}, ), ], child: ChildA(), ) |
BlocConsumer
✅ 新しい状態に反応するためにbuilderとlistenerを備えている
✅ ネストされたBlocListenerとBlocBuilderを簡潔なコードで表現したもの
✅ UIを再構築と、Blocの状態変化に対して別の処理を実行する両方が必要な場合にのみ使用する
✅ BuildWidgetBuilder, BuildWidgetListener, bloc指定、BlocBuilderCondition, BlocListenerConditionを取る
1 2 3 4 5 6 7 8 |
BlocConsumer<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, builder: (context, state) { // return widget here based on BlocA's state } ) |
✅ オプションであるlistenWhenとbuildWhenを実装することでより、listener, builderが呼び出されるタイミングを細かく制御することができる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
BlocConsumer<BlocA, BlocAState>( listenWhen: (previous, current) { // return true/false to determine whether or not // to invoke listener with state }, listener: (context, state) { // do stuff here based on BlocA's state }, buildWhen: (previous, current) { // return true/false to determine whether or not // to rebuild the widget with state }, builder: (context, state) { // return widget here based on BlocA's state } ) |
BlocSelector
✅ 状態変化に条件を付与してbuilder関数を走らせることができる
✅ Blocbuilderは状態変化があると必ずリビルドされるが、監視する値を制限(フィルタリング)することで、不要なビルドを回避することができる
1 2 3 4 5 6 |
BlocSelector<BlocA, BlocAState, bool>( selector: (state) => // BlocAの監視する状態(bool)を記載(e.g. state.user.isAuthenticated) builder: (context, isAuthenticated) { // do stuff here based on BlocA's state } ) |
RepositoryProvider
BlocProviderと用法は同じため省略
MultiRepositoryProvider
MultiBlocProviderと用法は同じため省略
Example of flutter_bloc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<meta charset="utf-8">// main.dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'screen transition', home: BlocProvider( create: (_) => CounterBloc(), child: CounterPage(), ), ); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// counter_bloc.dart abstract class CounterEvent {} class Increment extends CounterEvent {} class Decrement extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((event, emit) => emit(state + 1)); on<Decrement>((event, emit) => emit(state - 1)); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// counter_page.dart class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('CounterApp')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Center( child: Text( '$count', style: TextStyle(fontSize: 30.0), ), ); }, ), floatingActionButton: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.add), onPressed: () => context.read<CounterBloc>().add(Increment()), ), ), Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.add), onPressed: () => context.read<CounterBloc>().add(Decrement()), ), ), ], ), ); } } |
コメントを残す