Decolator パターンで Widget を構築する
はじめに
最近 Flutter でとある電卓アプリを作っているのですが、Flutter の宣言的 UI と Decolator
パターンは相性が良いのでは?と考え採用してみました。
あと、実際のプロダクトコードの一部を書いているのでちょっとグダグダ長くなってしまっているのですが、本筋だけ見てもらえると幸いです🙇♂️
ちなみにこんな感じの電卓を作っていますw
今回はこの電卓のキーを例にしたいと思います。
尚、(できるだけ)シンプルにするためにプロダクトコードよりは大きく情報を落としていますので、ご了承ください。
Decolator パターンとは
超簡単に表現するとある Component
に対して Decoration
(装飾) するという感じです。
イメージ
- スポンジケーキがある
- そこに生クリームとイチゴをトッピングするとショートケーキになるし、チョコソースでコーディングするとチョコレートケーキになる
以上!!!!🤣
っていうのは前回の C++ の記事でも説明しました。
もう少し具体的に表現すると Decolator
が Component
に機能を追加していく形になります。
雑な設計イメージ
冒頭で述べましたが、今回は電卓のキーを作るイメージで実装します。
-
Component
/Decolator
を実装 - 1 を組み合わせる Widget を実装(
Component
をStack
で積み上げていく) - 2 の Widget を利用して電卓のキーを作る
Decolator パターンのメリット
- パーツとなる
Component
クラスを用意すれば、専用のカスタムクラスを随時作らなくて良い - ベースクラスにそのまま拡張機能を持たせる場合、ベースクラスが変更されると全体に影響が出てしまうが、
Decorator
パターンは影響が少なくて済む - 組み合わせや順序は自由にできる
- 宣言時で機能や Widget が決定するので Flutter と相性が良い(たぶん)
Decolator パターンの Widget を構築してみる
というわけで実装に入りたいと思います。
ベースクラスの定義
まずは Decolator
パターンで使う全てのベースとなるクラスを実装します。
abstract class CalcItemPart {
CalcItem? calcItem;
CalcItemPart({this.calcItem});
List<Widget> buildParts();
}
buildParts
メソッドで Widget を構築させていく感じです。
CalcItem
/ CalcItemType
が存在しますが、本筋とはあまり関係ないのでさらっと見てもらうかスルーしてもらって大丈夫です(一応、コードは載せておきます)。
abstract class CalcItem with _$CalcItem {
const factory CalcItem (
CalcItemType type,
String value,
) = _CalcItem;
static CalcItem create(CalcItemType type) {
switch(type) {
case CalcItemType.Number_0: return CalcItem(type, '0');
case CalcItemType.Number_1: return CalcItem(type, '1');
case CalcItemType.Number_2: return CalcItem(type, '2');
case CalcItemType.Number_3: return CalcItem(type, '3');
case CalcItemType.Number_4: return CalcItem(type, '4');
case CalcItemType.Number_5: return CalcItem(type, '5');
case CalcItemType.Number_6: return CalcItem(type, '6');
case CalcItemType.Number_7: return CalcItem(type, '7');
case CalcItemType.Number_8: return CalcItem(type, '8');
case CalcItemType.Number_9: return CalcItem(type, '9');
case CalcItemType.AC: return CalcItem(type, 'AC');
case CalcItemType.Back: return CalcItem(type, '←');
case CalcItemType.Keyboard: return CalcItem(type, '');
case CalcItemType.Plus: return CalcItem(type, '+');
case CalcItemType.Minus: return CalcItem(type, '−');
case CalcItemType.Multiple: return CalcItem(type, '×');
case CalcItemType.Division: return CalcItem(type, '÷');
case CalcItemType.Dot: return CalcItem(type, '・');
case CalcItemType.PlusMinus: return CalcItem(type, '±');
case CalcItemType.Equal: return CalcItem(type, '=');
}
}
}
enum CalcItemType {
Number_0,
Number_1,
Number_2,
Number_3,
Number_4,
Number_5,
Number_6,
Number_7,
Number_8,
Number_9,
AC,
Back,
Keyboard,
Plus,
Minus,
Multiple,
Division,
PlusMinus,
Dot,
Equal
}
extension CalcItemTypeExt on CalcItemType {
CalcItemFont get fontInfo {
// 省略:フォントの情報を取得する.
}
}
Decolator の定義
次に Decolator
の定義です。 Decolator
は Component
を持っており、 Decolator
と Component
は同じベースクラスです。
abstract class CalcItemDecorator extends CalcItemPart {
CalcItemPart? component;
CalcItemDecorator({this.component}) : super(calcItem: component?.calcItem);
}
これで下準備は完了です。次はパーツを作っていきましょう!
各パーツの実装
各パーツは CalcItemDecorator
をベースにします。
class CalcItemText extends CalcItemDecorator {
CalcItemText({CalcItemPart? component}) : super(component: component);
List<Widget> buildParts() {
if (calcItem == null) return [];
final font = calcItem!.type.fontInfo;
final widget =
Center(child:
Text(calcItem!.value,
style: TextStyle(
color: Colors.white,
fontSize: font.size,
fontFamily: font.family,
fontWeight: font.weight
),
textAlign: TextAlign.center,
));
if (component == null) {
return [widget];
}
var parts = component!.buildParts();
parts.add(widget);
return parts;
}
}
typedef OnPressedCalcKey = void Function(CalcItemType itemType);
class CalcItemButton extends CalcItemDecorator {
OnPressedCalcKey? _onPressed;
CalcItemButton({CalcItemPart? component, OnPressedCalcKey? onPressed}) : super(component: component) {
_onPressed = onPressed;
}
List<Widget> buildParts() {
// TextButton は仮.
final button = TextButton(
onPressed: () {
// let は 自作拡張関数です.
component?.calcItem?.let((it) {
_onPressed?.let((run) => run(it.type));
});
},
child: Container()
);
if (component == null) {
return [button];
}
var parts = component?.buildParts();
parts?.add(button);
return parts!;
}
}
root 用のクラスを実装します。
class RootCalcItem extends CalcItemPart {
RootCalcItem({CalcItem? calcItem}) : super(calcItem: calcItem);
List<Widget> buildParts() {
return [];
}
}
Component を構成する Widget の実装
Stack
で Widget を組み合わせていきます。
class CalcItemWidget extends StatelessWidget {
CalcItemPart? _component;
CalcItemWidget(this._component);
Widget build(BuildContext context) {
if (_component == null) return Container();
return Center(
child: Container(
height: double.infinity,
width: double.infinity,
child: Center(child:
Stack(
fit: StackFit.expand,
children: _component!.buildParts(),
alignment: Alignment.center,
)),
),
);
}
}
Widget を組み合わせていく
最後に定義した Widget を組み合わせていきます。
かなり雑で申し訳ないですが、 _mode == 1
にはボタンがあり、 _mode == 0
のときはボタン機能がない見た目だけのキーを実装します。
_calcItemTypeList
には CalcItemType
が格納されておりこれをベースに電卓のキーが構成されていく感じです。
class CalculateKeyWidget extends StatelessWidget {
List<CalcItemType> _calcItemTypeList
int _mode;
CalculateKeyWidget(this._calcItemTypeList, this._mode);
Widget build(BuildContext context) {
final List<Widget> keys;
switch (_mode) {
case 1: keys = _getCalcKeys(_calcItemTypeList); break;
default: keys = _getCalcKeysWithoutButton(_calcItemTypeList); break;
}
final keyHeight = 80;
return
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: keyHeight,
child: Row(
children: [
keys[0], keys[1], keys[2], keys[3],
],
),
),
Container(
height: keyHeight,
child: Row(
children: [
keys[4], keys[5], keys[6], keys[7],
],
),
),
Container(
height: keyHeight,
child: Row(
children: [
keys[8], keys[9], keys[10], keys[11],
],
),
),
Container(
height: keyHeight,
child: Row(
children: [
keys[12], keys[13], keys[14], keys[15],
],
),
),
Container(
height: keyHeight,
child: Row(
children: [
keys[16], keys[17], keys[18], keys[19],
],
),
)
]);
}
List<Widget> _getCalcKeys(List<CalcItemType> calcItemTypeList) {
List<Widget> result = [];
calcItemTypeList.forEach((element) {
final widget =
CalcItemWidget(
CalcItemButton(
component: CalcItemText(
component: RootCalcItem(calcItem: CalcItem.create(element))
),
onPressed: (type) {
print(type);
}
)
);
result.add(widget);
});
return result;
}
List<Widget> _getCalcKeysWithoutButton(List<CalcItemType> calcItemTypeList) {
List<Widget> result = [];
calcItemInfoList.forEach((element) {
final widget =
CalcItemWidget(
CalcItemText(
component: RootCalcItem(calcItem: CalcItem.create(element))
),
);
result.add(widget);
});
return result;
}
}
以上が Decolator
パターンによる実装例となります。
補足
今回はなるべくシンプルに伝えたかったのでパターンがかなり少ないですが、プロダクトコードでは CalcItemType
が Keyboard
の時は CalcItemText
を使用せずに CalcItemIcon
というのを用意しアイコンを乗せるようにしたり、あるいは CalcItemImage
などを作って画像を乗せたりすることも可能になります。
Decolator
パターンは下準備が必要とはなりますが、一度作ってしまうとカスタマイズがしやすく、生成時に機能を決定できるので宣言的 UI と相性が良いと思っています。
もし採用できそうなら一度試してみる価値はあるかなと個人的には思います。
終わり(どうでもいいこと)
Flutter での開発は業務経験ではないのですが、宣言的 UI の素晴らしさはすでに結構堪能しています。なんというかデータを基準にするみたいな感覚で作っていて爽快感があり、また自ずと設計に対して妥協がしにくくなるので、色々と試される気がします。
最後まで読んでもらってありがとうございました!
電卓アプリもリリース予定なので良かったら遊んでみてくださいね(╹◡╹)
Discussion