LoginSignup
1
3

BLoCパターンを理解する

Last updated at Posted at 2024-05-14

BLoCパターンとは

BLoC(Business Logic Component)パターンとは、アプリケーションのビジネスロジックを
UIから分離するためのデザインパターンのことで、このパターンを採用することで、
アプリケーションの状態管理が容易になり、UIとビジネスロジック間の依存関係を
緩和することができます。

Bloc最大の特徴は Stream を活用していることです。
Stream を活用することで状態を監視し、リアクティブにUI側を更新することができます。

Blocの基本概念

BLoCパターンは、主に以下の4つの要素で構成されています。

  1. ビジネスロジックの分離: BLoCパターンでは、アプリケーションのビジネスロジックを
    UIから切り離します。これにより、ビジネスロジックの再利用性が向上し、
    UIコンポーネントが単純化され、コードが簡潔になります。

  2. イベント駆動: UIからの入力やアクションをイベントとして受け取り、
    それに基づいて状態を変更します。そのため、入力やアクションなどの
    イベントとして定義した機能をより柔軟に制御することができます。

  3. 状態管理: Blocは、アプリケーションの状態を管理します。
    アプリケーションの状態が変化するたびに、UIが更新されるため、
    アプリケーション全体の状態を一貫して追跡することができます。

  4. 単一責任の原則: BLoCパターンは、単一責任の原則に基づいています。
    各Blocは、特定のビジネスロジックに責任を持ち、その責務を明確にします。
    これにより、コードの保守性や拡張性が向上します。

Stream

Blocを理解する前に、まず Stream を理解する必要があります。
Stream とは小川という意味で、小川の上流でデータを流して、
下流でデータを受け取るというイメージになります。

では Stream を使用するとどのようなメリットがあるのでしょうか?

  1. 非同期処理の扱いが簡単
    非同期で連続したデータの流れを処理できるため、非同期処理を扱いやすくなります。
    Future などと比べると、 Stream を使えば複数回のデータ受信が可能で、
    より柔軟に対応することができます。

  2. メモリ効率が良い
    イベントがあった時だけデータを発行するため、メモリ効率が良くなります。
    毎回全てのデータを更新する必要がないので、パフォーマンスの面でも有利です。
    小川のイメージにもあるように、Streamに流れるデータのみを監視しておけば良いので、
    データを入れる処理とデータを取り出す処理が互いに意識する必要がありません。
    自身のタイミングで処理を行うことができます。

Stream実例

以下のコードは Stream を使ったシンプルなサンプルコードです。
このコードでは、1から10までの数値を順番に生成し、
それを Stream を通じて1秒ごとに出力しています。

import 'dart:async';

void main() {
  final streamController = StreamController<int>();

  // Streamを生成する
  final stream = streamController.stream;
  
  streamController.sink.add(1);

  // 1秒ごとに数値をStreamに送信する
  Timer.periodic(Duration(seconds: 1), (timer) {
    final value = timer.tick + 1;
    if (value <= 10) {
      streamController.sink.add(value);
    } else {
      timer.cancel();
      streamController.close();
    }
  });

  // Streamから数値を受信して出力する
  stream.listen((value) {
    print(value);
  }, onDone: () {
    print('Stream is closed');
  });
}

このコードでは、 StreamController を使用して Stream を作成し、
1秒ごとに1から10までの数値を送信しています。
StreamController は、 Stream を制御するためのクラスです。
<int> は、この Streamint 型のデータを扱うことを示しています。

sink は、Streamにデータを追加するためのインターフェースです。

数値を送信した後は、 listen メソッドを使用して Stream から数値を受信し、
それをコンソールに出力しています。

Stream がすべての数値を送信し終えると、
'Stream is closed'というメッセージが表示されます。

上記のコードを実行すると以下のように出力されます。

1
2
3
4
5
6
7
8
9
10
Stream is closed

Streamまとめ

Stream は、川の流れのようなもので、川の上流から水が絶え間なく下流に
向かって流れていきます。この水の流れが、 Stream がデータを発信する
様子に例えられています。

川の下流に住む人々は、この水の流れから必要な水を汲み上げて利用することができます。
これが、プログラムが Stream からデータを受け取り、処理を行うことに例えられます。

この川の流れには、以下のような特徴があります。

  • 水は一度に全て流れ着くのではなく、少しずつ継続して流れ続けます。
    Stream も同様に、データを一括で受け取るのではなく、
    少しずつ継続して受け取ることができます。

  • 川の流れは止まることなく絶え間なく続きます。 
    Stream も同様に、データの発信が継続する限り、
    プログラムはデータを絶え間なく受け取り続けることができます。

  • 川の流れは、上流で新しい水が加わると、下流の人々に新しい水が届きます。
    Stream も同様に、新しいデータが発信されると、
    プログラムにリアルタイムで新しいデータが届きます。

  • 川の流れが枯れた場合、下流の人々は水を得られなくなります。
    Stream でもデータの発信が止まれば、プログラムはそれ以上データを
    受け取ることができなくなります。

このように、Streamは継続的にデータを発信し、プログラムが発信されるデータを
絶え間なく受け取ることができる仕組みです。他方のことを深く考慮せずとも、
データの入出力ができるため、相互の変更による影響が受けにくく、リアルタイムで
データの変更を受け取れるので、アプリの状態を常に最新に保つことができます。

BLoCパターン 主な要素

BLoCパターンは上記の Stream の機能を使用して、ビジネスロジックを
独立したコンポーネントとして抽象化する設計パターンです。

では、どういった要素でBLoCパターンは構成されるのでしょうか?
BLoCは大きく分けて以下の4つの要素で構成されています。

  1. イベント(Events): ユーザーのアクションやシステムからの通知など、
    アプリケーション内で発生する事象を担当します。

  2. 状態(States): アプリケーションの状態を管理し、UIの表示内容を決定します。

  3. Bloc: イベントと状態の間のマッピング(項目同士の紐付け)を管理し、
    ビジネスロジックを実装します。Blocはイベントを受け取り、
    状態を生成してUIに渡します。

  4. UI レイヤー: ユーザーインターフェースを構築し、
    Blocから提供される状態を受け取って表示します。

BLoCパターン実例

BLoCパターンは、 flutter_bloc パッケージを使用して、実装することが一般的ですが、
ここでは基本的なBLoCパターンの概念を説明するために
Flutter標準の機能を使ってサンプルコードを解説します。

Flutterでは、BLoCパターンを実装するための基本的な機能が提供されており、
それを利用してBlocを作成することができます。

flutter_bloc パッケージを使用する主な利点は、
BlocとUIレイヤー間のデータの受け渡しを簡素化し、
より簡潔なコードを記述できることです。
さらに、 StatefulWidgetStreamBuilder などのウィジェットを
使用せずに実装することができるため、
より直感的にUIを更新することができます。

以下は、カウンターアプリを例にしたBlocの実装です。

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

// BLoCのインターフェース
abstract class CounterBloc {
  void increment();
  void decrement();
  Stream<int> get counter;
}

// カウンターのBLoC
class CounterBlocImpl implements CounterBloc {
  int _counter = 0;

  // ストリームコントローラー
  final _counterController = StreamController<int>.broadcast();
  
  @override
  void increment() {
    _counter++;
    _counterController.sink.add(_counter);
  }

  @override
  void decrement() {
    _counter--;
    _counterController.sink.add(_counter);
  }

  @override
  Stream<int> get counter => _counterController.stream;
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterPage(),
    );
  }
}

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  late final CounterBloc _counterBloc;

  @override
  void initState() {
    super.initState();
    _counterBloc = CounterBlocImpl();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter App'),
      ),
      body: Center(
        child: StreamBuilder<int>(
          initialData: 0,
          stream: _counterBloc.counter,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return Text('Count: ${snapshot.data}',
                  style: TextStyle(fontSize: 24.0));
            } else {
              return CircularProgressIndicator();
            }
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: _counterBloc.increment,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
          SizedBox(height: 16),
          FloatingActionButton(
            onPressed: _counterBloc.decrement,
            tooltip: 'Decrement',
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Events(イベント)

このサンプルコードでは、イベントは明示的に定義されていませんが、 CounterBlocincrement()decrement() メソッドがイベントを表しています。
これらのメソッドが呼び出されたときに、状態が更新されます。

States(状態)

状態は、CounterBlocImpl クラスの _counter 変数によって表現されています。
この変数は現在のカウントの値を保持しており、カウンターの状態を示しています。

Bloc

Bloc は、 CounterBlocImpl クラスによって表現されています。
このクラスはカウンターのビジネスロジックを実装しており、
イベントの受け取りと状態の更新を行います。

UI レイヤー

UI レイヤーは、 CounterPage クラスによって表現されています。
このクラスは、ユーザーインターフェースを構築し、
Blocから提供された状態に基づいてUIを更新します。

具体的には、 StreamBuilder ウィジェットを使用してカウンターの状態を監視し、
その状態に応じてUIを更新します。
また、ボタンをタップすることでイベントを発生させ、Blocにそれを伝えます。

// BLoCのインターフェース
abstract class CounterBloc {
  void increment();
  void decrement();
  Stream<int> get counter;
}
  • CounterBloc クラスは、Blocのインターフェースを定義しています。
    increment()decrement() メソッドでカウンターの値を増減させ、
    counter Streamでカウンターの現在値を公開しています。
// カウンターのBLoC
class CounterBlocImpl implements CounterBloc {
  int _counter = 0;

  // ストリームコントローラー
  final _counterController = StreamController<int>.broadcast();
  
  @override
  void increment() {
    _counter++;
    _counterController.sink.add(_counter);
  }

  @override
  void decrement() {
    _counter--;
    _counterController.sink.add(_counter);
  }

  @override
  Stream<int> get counter => _counterController.stream;
}
  • CounterBlocImpl クラスでは、 CounterBloc インターフェースを
    実装しています。 _counter 変数でカウンターの値を保持し、
    StreamController を使ってカウンターの値を Stream として提供しています。
    また broadcast() メソッドを使用して、複数のリスナーが同じイベントを
    受信できるように設定しています。

  • increment()decrement() メソッドでは、 _counter の値を増減させた後、 _counterController.sink.add(_counter) でStreamに新しい値を追加しています。

  • counter ゲッターでは、カウンターの値をStreamとして返し、
    カウンターの変更を監視しています。

class _CounterPageState extends State<CounterPage> {
  late final CounterBloc _counterBloc;

  @override
  void initState() {
    super.initState();
    _counterBloc = CounterBlocImpl();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter App'),
      ),
      body: Center(
        child: StreamBuilder<int>(
          stream: _counterBloc.counter,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return Text('Count: ${snapshot.data}',
                  style: TextStyle(fontSize: 24.0));
            } else {
              return CircularProgressIndicator();
            }
          },
        ),
      ),
      floatingActionButton: Column(
  • initState() メソッドは、ウィジェットが初期化されたときに呼び出されます。 CounterBlocImpl クラスのインスタンスを作成して、
    _counterBloc フィールドに割り当ててます。

  • StreamBuilder<int> ウィジェットを使用して、
    _counterBloc のストリームからカウンターの値を受信し、UIを更新しています。
    カウンターの値がある場合は、Text ウィジェットにカウンターの値を表示し、
    データがまだ到着していない場合はローディングインジケーターを表示します。

まとめ

このコードでは、UIとビジネスロジックが明確に分離されています。
UIの役割は、Blocからカウンターの値をストリームとして受け取り、
表示することだけです。

一方、Blocはカウンターの値を管理し、インクリメントやデクリメントのロジックを
実装しています。このように責務が分離されているため、
BLoCパターンを用いて実装することで、コードの保守性とテスト容易性が向上します。
これを機にぜひBLoCパターンを用いて実装してみてください。

参考資料

告知

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

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

1
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
1
3