flutter_blocでBlocパターンを実装する Bloc編

# Bloc
`Bloc` とは、 `Cubit` の上位概念で、ビジネスロジックを含む
コンポーネントのことを指します。 `Bloc` を使用することで、イベントを受け取り、
受け取ったイベントに応じて状態を変更することができます。

:::note info
**BlocパターンのBlocとは？**
Blocパターンでは `States` と `Events` を使って状態を管理します。
その状態とイベントを管理する際に、使用されるのが `Bloc` です。
`Bloc` は、 `Events` を受け取り、結果を `States` に反映する役割を担っています。
:::

また、 イベント駆動型のアーキテクチャのため、
アプリケーションのビジネスロジックとUIを明確に分離することができ、

複数の状態を管理し、イベントに基づいて状態を遷移させるので、
テストが容易になり、コードの再利用性も向上します。

:::note warn
`Bloc` は `Cubit` と比較して、より複雑な状態管理が必要な場合に使用されます。
:::

## 特徴

- **複雑なビジネスロジックを処理できる:**
 複数のイベントを処理したり、非同期処理を扱ったりすることができます。
- **複数のBlocを組み合わせることができる:**
 複数のBlocを組み合わせることで、複雑な状態管理を実現することができます。
- **テストが実装しやすい:**
 ビジネスロジックとUIを明確に分離するため、テスト実装が容易になります。

## インストール
`Bloc` を使用する際には `flutter_bloc` パッケージをインストールする必要があります。
以下のリンクからパッケージをインストールしましょう。

https://pub.dev/packages/flutter_bloc

```yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.0
```

### flutter_blocパッケージとは
`flutter_bloc` パッケージを使うと、FlutterアプリケーションにBlocパターンを
簡単に導入することができるようになります。

:::note info
**Blocパターンとは?**
Blocパターンは、ビジネスロジックとUIを分離させ、
アプリケーションの状態管理を効率的に行うためのデザインパターンです。
:::


# Blocの構成要素
Bloc(Business Logic Component)は、主に以下の3つの要素から構成されています。

- **Events:** Blocに送信される入力を表すオブジェクト
- **States:** Blocの状態を表すオブジェクト
- **Blocクラス:** `Events` を受け取り、ロジックを実行し、  
`States` を更新してUIに通知する


## Events
`Events` とは、ユーザーの操作や外部からの入力を表すオブジェクトです。
`Bloc` は、受け取った `Events`に応じて対応するロジックを実行し、 `States`を更新します。
例えば、カウンターアプリの場合、カウンタの増加・減少のイベントが
`Events` に相当します。
 
```dart: Eventsの定義例
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}
```

上記の例では、 `CounterEvent` を継承した `IncrementEvent` と `DecrementEvent` を
定義しています。これらのイベントが `Bloc` に送信されると、
`Bloc` はそれぞれのイベントに対応するロジックを実行し、 `States` を更新します。

`Events` は、ユーザーの操作に限らず、タイマーの時間管理やAPI呼び出しの結果、
ディープリンクの起動など、様々な入力を定義することがあります。

### イベントを定義する際のポイント：

- **明確な命名：**
イベント名はその動作を明確に表すようにします。（IncrementEvent、DecrementEvent）

- **必要なデータの保持：**
イベントに必要なデータをプロパティとして保持し、
`Bloc` が適切に処理できるようにします。

- **抽象クラスの利用：**
共通の親クラス（抽象クラス）を作成し、具体的なイベントごとにサブクラスを
作成することで、コードの拡張性を高めます。

アプリケーションの要件に応じて適切に `Events` を設計することが、
Blocパターンを効果的に活用するための鍵となります。

## Statesとは
`States` は、Blocの状態を表すオブジェクトです。
`Bloc` はイベントを受け取ると、ロジックに基づいて新しい状態を
生成し、その状態をUIに通知します。
UIは通知された新しい状態に基づいて、ウィジェットを再構築します。

```dart: Stateの定義例
class CounterState {
  final int count;
  CounterState(this.count);
}
```

上記の例では、 `CounterState` クラスが状態を表しています。
`count` フィールドにカウンターの値を保持しています。

単純なアプリでは上記のように整数値だけでも良いかもしれませんが、
より複雑なアプリケーションでは、複数のフィールドを持つクラスや、
ネストされたオブジェクトで状態を表現する必要があります。

適切に `States` を設計することで、以下のようなメリットがあります。
- **状態を明確に定義できる**
- **状態の一部分だけを更新することができる**
- **ネストされたオブジェクトでも状態を表現できる**
- **状態のコピーや比較が容易になる**

また、 `States` を定義する際は、イミュータブル(不変)なオブジェクトとして扱うことで、
状態の変更を制御でき、予期せぬ副作用を防ぐことができます。



## Blocクラス
Blocクラスには、イベントを受け取り、ロジックを実行して、
新しい状態を生成する処理が含まれています。
また、 状態の変化をUIに通知する機能も持っています。

```dart
// BlocクラスCounterBloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0));

  @override
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    if (event is IncrementEvent) {
      yield CounterState((state.count + 1));
    } else if (event is DecrementEvent) {
      yield CounterState((state.count - 1));
    }
  }
}
```

上記の例では、 `CounterBloc` クラスが `Bloc` クラスを
継承しています。`Bloc` クラスはジェネリック型で、
**第1型引数にイベントの型、第2型引数に状態の型**を指定します。

- `CounterBloc` のコンストラクタでは、
初期状態として `CounterState(0)` を指定しています。

- `mapEventToState` メソッドは、イベントを受け取り、
ロジックを実行して新しい状態を生成する役割を担っています。

- `IncrementEvent` が来た場合は、カウンターの値を `1` 増やした新しい `CounterState` を
`yield` で生成します。

- 同様に、 `DecrementEvent` が来た場合は、カウンターの値を `1` 減らした新しい
 `CounterState` を `yield` で生成しています。

:::note info
`flutter_bloc` パッケージでは、 `Bloc` クラスが `Stream` を
内部で管理しています。 `mapEventToState` で新しい状態を `yield` すると、
その状態がストリームに追加されるため、ストリームの管理が容易になります。

また、 `flutter_bloc` パッケージには、 `Bloc` クラスをUIで
使用するためのウィジェットも提供されているため、これらのウィジェットを
使用すると、 `Bloc` パターンを簡単に実装できます。
:::

`Bloc` クラスの定義に関しては、上記の `Bloc` クラス以外にも
以下のような定義方法も存在します。
```dart
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<Increment>((event, emit) => emit(CounterState(state.counter + 1)));
    on<Decrement>((event, emit) => emit(CounterState(state.counter - 1)));
  }
}
```

先ほどのコードは、　`mapEventToState` メソッドをオーバーライドして、
イベントに対するロジックを定義していた一方で、このコードでは、
 `on<Event>` を使用して、イベントに対するロジックを定義しています。

### 使い分け

**mapEventToState:**
`mapEventToState` は `Bloc` 内でイベントに対する処理を記述する従来の方法です。
イベントごとに処理を記述し、新しい状態を発行します。

```dart
@override
Stream<CounterState> mapEventToState(
  CounterEvent event,
) async* {
  if (event is IncrementEvent) {
    yield CounterState(state.count + 1);
  } else if (event is DecrementEvent) {
    yield CounterState(state.count - 1);
  }
}
```

`mapEventToState` は `Stream<State>` を返す関数です。
`yield` キーワードを使って新しい状態を生成します。
イベントごとの処理をif文やswitch文で分岐させる必要があります。

**`on<Event>`:**
`on<Event>` メソッドは `mapEventToState` 以降に導入された新しい方法です。
イベントの種類ごとにコールバック関数を登録することで、
コードの可読性が向上します。

```dart
on<IncrementEvent>((event, emit) {
  emit(CounterState(state.count + 1));
});

on<DecrementEvent>((event, emit) {
  emit(CounterState(state.count - 1));
});
```

`on<Event>` にイベントの型を指定し、コールバック関数を登録します。
コールバック関数の引数には、イベントオブジェクト(`event`)と状態を発行するための
関数(`emit`)が渡されます。 `emit` 関数を呼び出して新しい状態を発行します。


`mapEventToState` は従来の方法で、旧バージョンとの互換性のためにサポートされています。
`on<Event>` は新しい方法で、コードの可読性と保守性が向上するため、
`Bloc` クラスを定義する際はこちらを使用することが推奨されています。

- イベントの種類が少ない場合は `mapEventToState` でも問題ありませんが、
イベントの種類が増えるにつれて `on<Event>` の方が望ましくなります。

- ネストされたif文やswitch文が複雑になる場合は、 
`on<Event>`を使うことでコードがシンプルになります。

状況に応じて適切な方法を使い分けることが重要です。
`flutter_bloc` パッケージでは柔軟性の高い実装が可能で、
アプリケーションの要件に合わせて最適な方法を選択できます。

## Blocの動作の流れ

Blocの動作の流れを以下のコードをもとに説明します。

```dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// 1. Eventsの定義
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

// 2. Statesの定義
class CounterState {
  final int count;
  CounterState(this.count);
}

// 3. Blocの定義
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    on<IncrementEvent>((event, emit) {
      emit(CounterState(state.count + 1));
    });

    on<DecrementEvent>((event, emit) {
      emit(CounterState(state.count - 1));
    });
  }
}

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => CounterBloc(),
        child: const CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BlocBuilder<CounterBloc, CounterState>(
              builder: (context, state) {
                return Text(
                  'Count: ${state.count}',
                  style: const TextStyle(fontSize: 24),
                );
              },
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    context.read<CounterBloc>().add(IncrementEvent());
                  },
                  child: const Text('Increment'),
                ),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: () {
                    context.read<CounterBloc>().add(DecrementEvent());
                  },
                  child: const Text('Decrement'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```

### **イベントのディスパッチ:**
UIやその他のコンポーネントから、 `Bloc` にイベントがディスパッチされます。
上記の例では、 `onPressed` 内でそれぞれ対象のメソッドを呼び出すことで、
`IncrementEvent` と `DecrementEvent` を `Bloc` にディスパッチしています。

### **イベントの処理:**
Blocは受け取ったイベントを処理し、対応するビジネスロジックを実行します。
上記の例では、 `CounterBloc` クラスの `on<IncrementEvent>` と
 `on<DecrementEvent>` でイベントに対応するロジックが定義されています。

- `IncrementEvent` が来た場合、 `state.count + 1` で新しいカウント値を計算し、
- `DecrementEvent` が来た場合、 `state.count - 1` で新しいカウント値を計算します。

### **新しい状態のemit:** 
イベントの処理結果として、新しい状態を生成しています。
`Bloc` では `emit()`関数を使って新しい状態を生成します。
上記の例では、 `emit(CounterState(newCount))` で
新しい `CounterState` を生成しています。

### **状態の監視:**
`BlocProvider` を使って `CounterBloc` のインスタンスを作成し、
`CounterPage`に提供しています。

 `BlocBuilder` を使って `CounterBloc`の状態を監視し、状態が変更されるたびに
UIを再構築しています。 `ElevatedButton` を使って、 `IncrementEvent` と `DecrementEvent` を`Bloc` に追加しています。

この一連の流れを繰り返すことで、Blocパターンによる状態管理が実現されます。

# まとめ
`Bloc` はビジネスロジックを含むコンポーネントです。
`Events` を受け取り、ロジックに基づいて `States` を更新します。

UIとビジネスロジックを分離することができ、
テスト容易性やコードの再利用性が高まります。

**Blocの全体の流れ**

- UIやその他のコンポーネントからイベントがBlocにディスパッチされる
- イベントを受け取り、対応するビジネスロジックを実行する
- ビジネスロジックの結果として新しい状態を生成し、その状態をエミットする
- UIコンポーネントはBlocからの状態の変更を監視し、必要に応じてUIを更新する

Blocを使用することで、複雑なビジネスロジックを持つアプリケーションでも、
可読性が高く、保守しやすいコードを実現することができます。

これを機にぜひ使用してみてください！

# 告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。

https://eda-inc.jp/recruit/

みなさまからのご応募をお待ちしております。