実践におけるブロックパターンの適用


ブロックパターンの目的を説明するWeb上で多数のドキュメントがあり、それが最小限の例に適用されます.このチュートリアルでは、この状態管理アーキテクチャに既存のアプリケーションの移行を目指します.
そのためには、calculator app 誰の創造について説明しました.全体のアプリの状態が管理されてStatefulWidget . 全体の状態をビジネスロジックからUIの分離に繋がるブロックによって管理させるように調整しましょう.

動機


なぜ、我々は第一にこのような移行を望みますか?すべてが働く限り、私たちはそのようにそれを保つことができます.
もちろん、現在の実装は機能します.しかし、我々が何かを変えるならば、我々は機能が壊れないことを確実に言うことができませんでした.
この問題はまったく新しいことではない.それが人々が発明した理由ですsoftware testing . しかし、今、我々はコアの質問に来ます.我々が現在の実装を確実にしたいならば、我々の単位テストはどのように見えますか?
答えはそうではない.私たちのウィジェットには2つの責任があります:UIの表示とビジネスロジックの定義.UIの部分を変更すると、厳密には分離されないので、ビジネスロジックを壊す可能性があります.他の方法も同様です.
Uncle Bob 'それに対する意見:責任は変化する理由として定義される.すべてのクラスまたはモジュールは、変更されるべき1つの理由がなければなりません.

実装


BLOCパターンを実装することは多くのboilerplateコード(BLOC、イベント、状態、およびその抽象論理のすべて)を含みます.そういうわけで、我々は我々を簡単にして、プレハブ化された解決を使いますbloc library . パッケージには、パターンを実装するのを防ぐいくつかのクラスが含まれます.
プロジェクトに依存関係を追加します.
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^6.0.3
  equatable: ^1.2.5

イベント


イベントから始めましょう.UIを垣間見ることによって、私たちが必要なイベントを実現することができます.

ユーザーが4つの異なる相互作用の結果を押すことができるボタンの4種類があります
  • 数字ボタン( 0 - 9 )を押す
  • 演算子ボタン(+,-, x ,/)を押す
  • 結果計算ボタン(=
  • "clear "ボタンを押す

  • これらの正式な要件をブロックコンテキストのイベントに翻訳しましょう.
    import 'package:equatable/equatable.dart';
    import 'package:meta/meta.dart';
    
    abstract class CalculationEvent extends Equatable {
      const CalculationEvent();
    }
    
    class NumberPressed extends CalculationEvent {
      final int number;
    
      const NumberPressed({@required this.number}) : assert(number != null);
    
      @override
      List<Object> get props => [number];
    }
    
    class OperatorPressed extends CalculationEvent {
      final String operator;
    
      const OperatorPressed({@required this.operator}) : assert(operator != null);
    
      @override
      List<Object> get props => [operator];
    }
    
    class CalculateResult extends CalculationEvent {
      @override
      List<Object> get props =>  [];
    }
    
    class ClearCalculation extends CalculationEvent {
      @override
      List<Object> get props =>  [];
    }
    
    
    パラメータを必要とする唯一のイベントはNumberPressed and OperatorPressed (私たちがどちらが取られたかを区別する必要があるので).他の2つのイベントにはプロパティはありません.


    国はかなり簡単です.それは私たちのUIは何が正確に起こった気にしないからです.全体の計算(2つのオペランド、演算子と結果からなる)が変化するとき、それは状況に注意します.そういうわけで、我々は計算が変わったと言う1つの実際の状態を必要とするだけです.また、1つの初期状態が必要です.私たちには非同期操作が全くないので、私たちは「ローディング」状態を必要としません.
    import 'package:equatable/equatable.dart';
    import 'package:meta/meta.dart';
    
    import '../calculation_model.dart';
    
    abstract class CalculationState extends Equatable {
      final CalculationModel calculationModel;
    
      const CalculationState({@required this.calculationModel}) : assert(calculationModel != null);
    
      @override
      List<Object> get props => [calculationModel];
    }
    
    class CalculationInitial extends CalculationState {
      CalculationInitial() : super(calculationModel: CalculationModel());
    }
    
    class CalculationChanged extends CalculationState {
      final CalculationModel calculationModel;
    
      const CalculationChanged({@required this.calculationModel})
          : assert(calculationModel != null),
            super(calculationModel: calculationModel);
    
      @override
      List<Object> get props => [calculationModel];
    }
    
    両方の状態のすべてのプロパティを複製する代わりに、私たちはCalculationModel . このように、何かを変更した場合(最後の結果を表示する場合など)、1つのモデルを変更するだけでよい.これはモデルがどのように見えるかです.
    import 'package:equatable/equatable.dart';
    
    class CalculationModel extends Equatable {
      CalculationModel({
        this.firstOperand,
        this.operator,
        this.secondOperand,
        this.result,
      });
    
      final int firstOperand;
      final String operator;
      final int secondOperand;
      final int result;
    
      @override
      String toString() {
        return "$firstOperand$operator$secondOperand=$result";
      }
    
      @override
      List<Object> get props => [firstOperand, operator, secondOperand, result];
    }
    
    このモデルを拡張することが重要ですEquatable . 同様に州に行く.これは何かが実際に変更されたときのみ変更するUIを必要とするためです.DARTが変更があったのかどうかを定義する必要があります.これは小道具を定義することによって行われます.

    ブロック


    class CalculationBloc extends Bloc<CalculationEvent, CalculationState> {
      CalculationBloc() : super(CalculationInitial());
    
      @override
      Stream<CalculationState> mapEventToState(
        CalculationEvent event,
      ) async* {
        if (event is ClearCalculation) {
          yield CalculationInitial();
        }
      }
    }
    
    
    オーバーライドする必要がある唯一の方法があります.mapEventToState() . このメソッドは、着信イベントを処理し、新しい状態を発行する責任があります.最も簡単なものから始めます.ClearCalculation . これにより、再び初期状態が生成されるCalculationInitial ).
    次のOperatorPressed イベント.
      Future<CalculationState> _mapOperatorPressedToState(
          OperatorPressed event,
          ) async {
        List<String> allowedOperators = ['+', '-', '*', '/'];
    
        if (!allowedOperators.contains(event.operator)) {
          return state;
        }
    
        CalculationModel model = state.calculationModel;
    
        return CalculationChanged(
          calculationModel: CalculationModel(
            firstOperand: model.firstOperand == null ? 0 : model.firstOperand,
            operator: event.operator,
            secondOperand: model.secondOperand,
            result: model.result
          )
        );
      }
    
    私たちは何かに気がつきます:私たちがモデルの新しいインスタンスを使用することは、重要です.リファレンスを使用して動作する場合、BLOCライブラリには、等号をチェックする問題があります.しかし、新しいインスタンスを古い値にする必要があるので、新しいCalculationState .
    いくつかの状態を結果として扱ういくつかのケースを扱うメソッドがあれば、これは多くのboilerplateコードのようです.だからこそ、我々はCalculationModel 方法によってcopyWith() :
      CalculationModel copyWith({
          int Function() firstOperand,
          String Function() operator,
          int Function() secondOperand,
          int Function() result
        })
      {
        return CalculationModel(
          firstOperand: firstOperand?.call() ?? this.firstOperand,
          operator: operator?.call() ?? this.operator,
          secondOperand: secondOperand?.call() ?? this.secondOperand,
          result: result?.call() ?? this.result,
        );
      }
    
    なぜ私たちは、日付型自体ではなく、関数を期待する自分自身を求める可能性があります.それ以外のNULL値は正確に指定されたパラメータとして扱われます.別の記事で詳しく説明します.
    今、我々は非常に同じ方法を書くことができます_mapOperatorPressedToState このように:
      Future<CalculationState> _mapOperatorPressedToState(
          OperatorPressed event,
          ) async {
        List<String> allowedOperators = ['+', '-', '*', '/'];
    
        if (!allowedOperators.contains(event.operator)) {
          return state;
        }
    
        CalculationModel model = state.calculationModel;
    
        CalculationModel newModel = state.calculationModel.copyWith(
          firstOperand: () => model.firstOperand == null ? 0 : model.firstOperand,
          operator: () => event.operator
        );
    
        return CalculationChanged(calculationModel: newModel);
      }
    
    の取り扱いを続けましょうCalculateResult イベント
      Future<CalculationState> _mapCalculateResultToState(
          CalculateResult event,
        ) async {
        CalculationModel model = state.calculationModel;
    
        if (model.operator == null || model.secondOperand == null) {
          return state;
        }
    
        int result = 0;
    
        switch (model.operator) {
          case '+':
            result = model.firstOperand + model.secondOperand;
            break;
          case '-':
            result = model.firstOperand - model.secondOperand;
            break;
          case '*':
            result = model.firstOperand * model.secondOperand;
            break;
          case '/':
            if (model.secondOperand == 0) {
              CalculationModel resultModel = CalculationInitial().calculationModel.copyWith(
                firstOperand: () => 0
              );
    
              return CalculationChanged(calculationModel: resultModel);
            }
            result = model.firstOperand ~/ model.secondOperand;
            break;
        }
    
    我々は、ロジックを適用すると、我々は変換前にした.ここで注目すべき点は、ゼロの除算が次の計算のための最初のオペランドとして“0”をもたらすことです.
    今、最大のイベントはNumberPressed 現在の計算の状態に応じて、数を押すと、さまざまな影響を与えることができます.そういうわけで、取り扱いはここで少し複雑です.
      Future<CalculationState> _mapNumberPressedToState(
        NumberPressed event,
      ) async {
        CalculationModel model = state.calculationModel;
    
        if (model.result != null) {
          CalculationModel newModel = model.copyWith(
            firstOperand: () => event.number,
            result: () => null
          );
    
          return CalculationChanged(calculationModel: newModel);
        }
    
        if (model.firstOperand == null) {
          CalculationModel newModel = model.copyWith(
            firstOperand: () => event.number
          );
    
          return CalculationChanged(calculationModel: newModel);
        }
    
        if (model.operator == null) {
          CalculationModel newModel = model.copyWith(
            firstOperand: () => int.parse('${model.firstOperand}${event.number}')
          );
    
          return CalculationChanged(calculationModel: newModel);
        }
    
        if (model.secondOperand == null) {
          CalculationModel newModel = model.copyWith(
            secondOperand: () => event.number
          );
    
          return CalculationChanged(calculationModel: newModel);
        }
    
        return CalculationChanged(
          calculationModel: model.copyWith(
            secondOperand: () =>  int.parse('${model.secondOperand}${event.number}')
          )
        );
      }
    
    しかし、再び、我々はちょうどブロックパターンのないアプリケーションのバージョンから元の動作をコピーしている.
    最後にすることは、ウィジェットにイベントを発行させて、ブロックの放出された状態に反応させることです.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Flutter basic calculator',
          home: Scaffold(
            body: BlocProvider(
              create: (context) {
                return CalculationBloc();
              },
              child: Calculation(),
            ),
          ),
        );
      }
    
    まず、メインで.ダート、我々は追加する必要がありますBlocProvider . これはウィジェットをブロックと通信できるようにするために必要です.
    さて、呼び出しの代わりにsetState UIとの相互作用を認識するたびに、イベントを生成します.
      numberPressed(int number) {
        context.bloc<CalculationBloc>().add(NumberPressed(number: number));
      }
    
      operatorPressed(String operator) {
        context.bloc<CalculationBloc>().add(OperatorPressed(operator: operator));
      }
    
      calculateResult() {
        context.bloc<CalculationBloc>().add(CalculateResult());
      }
    
      clear() {
        context.bloc<CalculationBloc>().add(ClearCalculation());
      }
    
    さて、結果を表示するにはUIをラップしたい場合はResultDisplayBlocBuilder これは、我々に放出された状態で反応する能力を与えます.
    BlocBuilder<CalculationBloc, CalculationState>(
      builder: (context, CalculationState state) {
        return ResultDisplay(
          text: _getDisplayText(state.calculationModel),
        );
      },
    ),
    ...
    String _getDisplayText(CalculationModel model) {
      if (model.result != null) {
        return '${model.result}';
      }
    
      if (model.secondOperand != null) {
        return '${model.firstOperand}${model.operator}${model.secondOperand}';
      }
    
      if (model.operator != null) {
        return '${model.firstOperand}${model.operator}';
      }
    
      if (model.firstOperand != null) {
        return '${model.firstOperand}';
      }
    
      return "${model.result ?? 0}";
    }
    

    最終語


    ブロックパターンは、ビジネスロジックの懸念からUIの懸念を分離する素晴らしい方法です.また、開発者は、無効なプレゼンテーション層としてのみウィジェットを残して状態を管理することができます.最初はちょっと混乱するかもしれませんが、一度基本的な考えを得ると、このパターンを適用するのは難しくありません.私は、この小さい例が少しそれを片付けたことを望みます.
    完全なソースが欲しいならば、(現在のマスター)の前に、そして、ブロックPRの後に倉庫があります:
    GET FULL CODE WITH BLOC
    GET FULL CODE WITHOUT BLOC