4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

Bloc

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

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

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

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

BlocCubit と比較して、より複雑な状態管理が必要な場合に使用されます。

特徴

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

インストール

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

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.0

flutter_blocパッケージとは

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

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

Blocの構成要素

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

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

Events

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

Eventsの定義例
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

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

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

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

  • 明確な命名:
    イベント名はその動作を明確に表すようにします。(IncrementEvent、DecrementEvent)

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

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

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

Statesとは

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

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

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

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

適切に States を設計することで、以下のようなメリットがあります。

  • 状態を明確に定義できる
  • 状態の一部分だけを更新することができる
  • ネストされたオブジェクトでも状態を表現できる
  • 状態のコピーや比較が容易になる

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

Blocクラス

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

// 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 減らした新しい
    CounterStateyield で生成しています。

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

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

Bloc クラスの定義に関しては、上記の Bloc クラス以外にも
以下のような定義方法も存在します。

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:
mapEventToStateBloc 内でイベントに対する処理を記述する従来の方法です。
イベントごとに処理を記述し、新しい状態を発行します。

@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);
  }
}

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

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

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の動作の流れを以下のコードをもとに説明します。

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 内でそれぞれ対象のメソッドを呼び出すことで、
IncrementEventDecrementEventBloc にディスパッチしています。

イベントの処理:

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 を使って、 IncrementEventDecrementEventBloc に追加しています。

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

まとめ

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

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

Blocの全体の流れ

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

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

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

告知

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

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?