🗳️

【Flutter/Dart】Bloc !! <アーキテクチャ>

2022/04/25に公開

概要

今日は、UIとビジネスロジックを分けて開発しやすくなる。
Blocについてやっていこうと思う。

処理の流れ

導入

https://pub.dev/packages/flutter_bloc/install

https://bloclibrary.dev/#/coreconcepts?id=cubit-vs-bloc

pubspec.yaml
dependencies:
  bloc: ^8.0.3
  flutter_bloc: ^8.0.1

こいつを記載して、flutter pub getを実行

実装してみた。

まずはフォルダ構成

lib/
  main.dart
  counter_page.dart
  cubit/
    counter_cubit.dart

UI部分

counter_page.dart
import 'package:counter_cubit/counter_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;
  
    
  Widget build(BuildContext context) {
    // BlocProviderでWidgetを囲む。
    return BlocProvider(
      // create でビジネスロジックをおきます
      // こうやって囲むと配下のWidgetでcreateのビジネスロジックが使えます。
      // contextに処理を格納してます。
      create: (_) => CounterCubit(),
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
	      // BlocBuilderで囲むことでStatefullWidgetのsetStateを実現しています。
              BlocBuilder<CounterCubit, int>(
                builder: (context, state) => Text(
                  '$state',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
	      // contextに格納されたクラスを呼び出すことでファンクションを実行します。
              onPressed: context.read<CounterCubit>().countUp,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
            const SizedBox(
              height: 8,
            ),
            FloatingActionButton(
              onPressed: context.read<CounterCubit>().countDown,
              tooltip: 'decriment',
              child: const Icon(
                Icons.exposure_minus_1,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

ビジネスロジック部分

counter_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';

// Cubitクラスをextendsします。<>の中にモデルなどを設定して使います。※今回はint
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void countUp() => emit(state + 1);

  void countDown() => emit(state - 1);
}

or

counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';

abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterdecrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
    on<CounterdecrementPressed>((event, emit) => emit(state - 1));
  }
}

画面遷移を伴う場合は

Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) => BlocProvider.value(
	value: BlocProvider.of<CounterCubit>(context),
	child: const SamplePage(),
      ),
    ),
  );

テスト

pubspec.yaml
dev_dependencies:
  bloc_test: ^9.0.3 #最新版
bloc_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:counter_cubit/counter_cubit.dart';
import 'package:flutter_test/flutter_test.dart';

//モック用のクラスを準備します。
class MockCounterCubit extends MockCubit<int> implements CounterCubit {}

void main() {
  mainCubit();
}

void mainCubit() {
  group('whenListen', () {
    test("Let's mock the CounterCubit's stream!", () {
      // Create Mock CounterCubit Instance
      final cubit = MockCounterCubit();

      // Stub the listen with a fake Stream
      whenListen(cubit, Stream.fromIterable([1, 2, 3, 4]));

      // Expect that the CounterCubit instance emitted the stubbed Stream of
      // states
      expectLater(cubit.stream, emitsInOrder(<int>[1, 2, 3, 4]));
    });
  });

  group('CounterCubit', () {
    blocTest<CounterCubit, int>(
      'emits [] when nothing is called',
      build: () => CounterCubit(),
      expect: () => const <int>[],
    );

    blocTest<CounterCubit, int>(
      'add \'Fish\' is called',
      build: () => CounterCubit(),
      act: (cubit) => cubit.countUp(),
      expect: () => const <int>[1],
    );

    blocTest<CounterCubit, int>(
      'remove \'Fish\'  is called',
      build: () => CounterCubit(),
      seed: () => 1,
      act: (cubit) => cubit.countDown(),
      expect: () => const <int>[0],
    );
  });
}

使ってみて

ビジネスロジックとUIを分けて実装できるので、それぞれ分担して作業することも可能なのでなかなか使い勝手が良さそうです。

他にもこうゆう、類のライブラリがあるので作りやすいので色々試して自分のアーキテクチャに合うものを使っていけたらいいかなというふうに感じます。

Discussion