虎視眈々と

Flutter × Firebaseを研究するアプリエンジニア

Flutterで有名なBLoCを使ってカウントアップアプリを作る

f:id:superman199323:20180913073236p:plain

Flutterで有名なBLoCとは

今回はBLoCについて書いていきます。

BLoCとは😦

Dart Conf 2018で発表されたアーキテクチャになります。

www.youtube.com

詳しくは上のYouTube動画でこの解説がなされています。 BLoCBusiness Logic Component の略です。

要は ビジネスロジックと画面のViewを明確に分けましょう というアーキテクチャです。

実装上の指針

具体的な指針は下記になります。

  • BLoCの入力・出力インターフェースはすべてStream/Sinkである
  • BLoCの依存は必ず注入可能で、環境に依存しない
  • BLoC内に環境ごとの条件分岐は持たない
  • 以上のルールに従う限り実装は自由である

ポイントはこの4つです。YouTube動画の中でもこのようなことをいわれています。

具体的な概要は下記のGoogle I/O 2018の動画で詳しく説明されています。

www.youtube.com

ソースコード

一番簡単にBlocパターンを実装

フローティングボタンを押すと画面中央の数字がカウントアップしていくだけの簡単なアプリケーションを例にして説明していきます。

f:id:superman199323:20180913065814g:plain

ソースコード

ソースコードを示して実践していきます。


import 'dart:async';

import 'package:bloc_sample_app/BlocProvider.dart';

class IncrementBloc implements BlocBase {

  int _counter;

  // handle controller
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  // handle view action
  StreamController<int> _actionController = StreamController<int>();
  StreamSink<int> get incrementCounter => _actionController.sink;

  IncrementBloc() {
    _counter = 0;
    _actionController.stream.listen(_handleLogic);
  }

  void _handleLogic(data) {
    _counter += 1;
    _inAdd.add(_counter);
  }

  @override
  void dispose() {
    _counterController.close();
    _actionController.close();
  }
}

Blocアーキテクチャの特徴として BLoCの入力・出力インターフェースはすべてStream/Sinkである というルールがあり、このクラスで入出力を管理します。

  • SInk : 入力イベント
  • Stream : 出力イベント

_actionController はコンストラクタの中でSink(入力)させるのを待って変更があると出力(Stream)定義をしています。 ですのでこのコンストラクタの中では _actionController.sink.add されると _handleLogic が呼ばれ、カウントアップします。

  • Viewクラス

次にViewからBlocクラスを呼んで入出力イベントを検知しましょう。

import 'dart:async';

import 'package:bloc_sample_app/BlocProvider.dart';
import 'package:bloc_sample_app/minimum/IncrementBloc.dart';
import 'package:flutter/material.dart';


class MinimumStateless extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<IncrementBloc>(
      bloc: IncrementBloc(),
      child: Minimum(),
    );
  }
}

class Minimum extends StatefulWidget {
  @override
  _MinimumState createState() => new _MinimumState();
}

class _MinimumState extends State<Minimum> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text("最小のBLoC"),
      ),
      body: Center(
        child: StreamBuilder<int>(
            stream: bloc.outCounter,
            initialData: counter,
            builder: (context, snapshot) {
              return Text("${snapshot.data}");
            }),
      ),
      floatingActionButton:
          FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
                bloc.incrementCounter.add(counter);
              }),
    );
  }
}

Blocのもう一つのルールとして BLoCの依存は必ず注入可能で、環境に依存しない があります。 ですので、依存を注入するためのクラス BlocProvider を使って依存を注入しています。(このクラスのコードは下ででてきます。)

body: Center(
   child: StreamBuilder<int>(
       stream: bloc.outCounter,
       initialData: counter,
       builder: (context, snapshot) {
          return Text("${snapshot.data}");
        }),
),

この部分で StreamBuilder クラスを使ってSink(入力イベント)がきたら出力するイベントを設定しています。

      floatingActionButton:
          FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
                bloc.incrementCounter.add(counter);
              }),

この部分でfloatingボタンを押すと入力イベントがおきます。 入力イベントが起きると、Blocクラスのコンストラクタで定義した Stream(出力イベント) が走り、画面の中央の数字がカウントアップされていくという仕組みです。

  • BlocProvider
import 'package:flutter/cupertino.dart';

abstract class BlocBase {
  void dispose();
}

// Generic BLoC provider
class BlocProvider<T extends BlocBase> extends StatefulWidget {
  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }): super(key: key);

  final T bloc;
  final Widget child;

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context){
    final type = _typeOf<BlocProvider<T>>();
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.bloc;
  }

  static Type _typeOf<T>() => T;
}

class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
  @override
  void dispose(){
    widget.bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return widget.child;
  }
}

Bolcアーキテクチャの利点

このアーキテクチャ採用することで、Viewとロジックが明確に別れてテストが書きやすくなります。 例えば先ほど書いた例で、「カウントアップするときに3の倍数だけScienceと表示したい」みたいな仕様が追加された場合、_handleLogic にテストがかけます。 もしこれがViewの中に書かれていたらテストが書くのは難しくなるかとおもいます。

また、このアーキテクチャはReactive Programingなので、Flutterでそれが実現できるのもいいですね。

サンプルコード

サンプルコードはこちらからどうぞ

github.com

このリポジトリの中では実際にAPIを叩いた時の実装も書いておりますのでその辺りも参考にしていただけると嬉しいです。

f:id:superman199323:20180913072730g:plain