実践におけるブロックパターンの適用
38864 ワード
ブロックパターンの目的を説明するWeb上で多数のドキュメントがあり、それが最小限の例に適用されます.このチュートリアルでは、この状態管理アーキテクチャに既存のアプリケーションの移行を目指します.
そのためには、calculator app 誰の創造について説明しました.全体のアプリの状態が管理されて
動機
数字ボタン( 0 - 9 )を押す 演算子ボタン(+,-, x ,/)を押す 結果計算ボタン(= "clear "ボタンを押す
これらの正式な要件をブロックコンテキストのイベントに翻訳しましょう.
国はかなり簡単です.それは私たちのUIは何が正確に起こった気にしないからです.全体の計算(2つのオペランド、演算子と結果からなる)が変化するとき、それは状況に注意します.そういうわけで、我々は計算が変わったと言う1つの実際の状態を必要とするだけです.また、1つの初期状態が必要です.私たちには非同期操作が全くないので、私たちは「ローディング」状態を必要としません.
ブロック
そのためには、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種類があります
BLOCパターンを実装することは多くのboilerplateコード(BLOC、イベント、状態、およびその抽象論理のすべて)を含みます.そういうわけで、我々は我々を簡単にして、プレハブ化された解決を使いますbloc library . パッケージには、パターンを実装するのを防ぐいくつかのクラスが含まれます.
プロジェクトに依存関係を追加します.
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^6.0.3
equatable: ^1.2.5
イベント
イベントから始めましょう.UIを垣間見ることによって、私たちが必要なイベントを実現することができます.
ユーザーが4つの異なる相互作用の結果を押すことができるボタンの4種類があります
これらの正式な要件をブロックコンテキストのイベントに翻訳しましょう.
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をラップしたい場合はResultDisplay
とBlocBuilder
これは、我々に放出された状態で反応する能力を与えます.
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
Reference
この問題について(実践におけるブロックパターンの適用), 我々は、より多くの情報をここで見つけました
https://dev.to/flutterclutter/applying-the-bloc-pattern-in-practice-1men
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
class CalculationBloc extends Bloc<CalculationEvent, CalculationState> {
CalculationBloc() : super(CalculationInitial());
@override
Stream<CalculationState> mapEventToState(
CalculationEvent event,
) async* {
if (event is ClearCalculation) {
yield CalculationInitial();
}
}
}
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
)
);
}
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,
);
}
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);
}
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;
}
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(),
),
),
);
}
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());
}
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
Reference
この問題について(実践におけるブロックパターンの適用), 我々は、より多くの情報をここで見つけました https://dev.to/flutterclutter/applying-the-bloc-pattern-in-practice-1menテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol