はじめに
学習当初に悩んでいたことでWidgetの作り方があります。
具体的には以下のことで悩んでいました。
- 画面を作っていくとビルド関数が肥大化して見づらい
- 画面を自分で決めたブロックを作って切り出す時って、どうしたらいい?
- ブロックごとに切り出してもメンテしづらい
ただただ動くだけのサンプルアプリであればビルド関数にバーっと書いてしまっていいでしょう。
しかし長期サービスでメンテナンスが必要であったり、複数人での開発で誰もが読みやすいコードにする必要な場合があると思います。
そこで、私が現場に入って学んだWidgetの構築方法を書いていきたいと思います。
ただし、現場によって規格が統一されていたり個人によってWidgetの切り出し方の理解のしやすさには差があると考えます。
よって、あくまで参考程度にしていただけたらと考えます。
尚、エディターはVSCodeを前提としています。
筆者の経歴
- UIKitでのiOSアプリケーション開発の学習9ヶ月
- SwiftUIでのiOSアプリケーション開発学習4ヶ月
- Flutterでのモバイルアプリケーション開発3ヶ月
記事の対象者
- Flutter初学者の方
- 画面の構築で悩んでいる方
記事を執筆時点での筆者の環境
- macOS 14.3.1
- Xcode 15.2
- Swift 5.9
- iPhone11 pro ⇒ iOS 17.2.1
- Flutter 3.19.0
- Dart 3.3.0
- Pixel 7a ⇒ Android 14
1. 前提
私がWidgetを構築している上で、コードを整理しようと考える判断基準は以下の内容です。
- コードが100行を超えている
- ネストが深くて可読性が悪い
- 他の画面でも共通で使いまわせる部分がある
ソースは以下からご覧ください
元ネタはNintendoSwitchOnlineのアプリでスプラトゥーン3用のイカリング3です。
各Widgetは見た目だけです。
本来は画像がスライドしたり、各ボタンから別画面へ遷移しますが今回はあくまで見た目だけ構築しています。
アプリ内のディレクトリー構造は以下のようになっています。
lib
├── no_refactor
│ └── no_refactor_screen.dart
├── refactor
│ ├── widgets
│ │ ├── action_item.dart
│ │ ├── friend_list_tile.dart
│ │ ├── image_slider_view.dart
│ │ ├── main_actions.dart
│ │ └── sub_actions.dart
│ └── refactor_screen.dart
└── main.dart
no_refactor_screen.dartのビルド関数にバーっと書いて364行になっています。
それをリファクタリングしたものがrefactorディレクトリ内に入っています。
今回の記事ではno_refactor_screen.dartをリファクタリングしていく様を書いていきたいと思います。
なお、Widgetの間に入れるスペースをgapというパッケージを使って入れています。
スペースを指定して入れたい場合に便利なパッケージです。
2. レイアウトに関する内容を隠蔽する
まず最初に画面全体に関するリファクタリングを行います。
レイアウトを司るクラスを作り画面のレイアウトに関わる内容は
全てそこに書いてしまいます。
こうすることでビルド関数内に表示されるのは何を表示させるかの対象になるべく限定されます。また、コードのネストが浅くなります。
2-1. レイアウトを司るクラスに切り出す
no_refactor_screen.dart
の Scaffold
を選択して「⌘ + .」(コマンド + ドット)
からExtract Widget
を選択してWidgetとして切り出します。
このクラスはこのファイル内でしか使わないことが確定しているのでアンダースコアをつけて
プライベートなクラスで宣言します。
命名は_NoRefactorScreenLayout
とします
2-3. レイアウト関連ではないものを引数にする:title編
ここでは上から順番に整理していきます。
最初にAppBarの部分です。
titleで表示している文字列’イカリング3’を引数にします。
変数final String appBarTitle;
で定義
コストラクターにも名前付き引数で宣言
この時、super.key
は現在使う予定がないので消しておいて大丈夫
_RefactorScreenLayout
の’イカリング3’の部分をappBarTitleに置き換え
(constの付け直しも忘れずに)
RefactorScreen
のビルド関数内で呼び出している_RefactorScreenLayout
の引数を追加
引数に’イカリング3’を追記
差分は以下
2-4. レイアウト関連ではないものを引数にする:actions編
次にactionsに入っているおわる
ボタンを変更していきます。
actionsは元々Widgetを配列で渡せるようになっています。
今後ここに他のWidgetが追加になるかもしれないことも考えて、ここの引数はWidgetの配列で渡すようにします。
先ほどの要領でやっていく結果は以下のようになります。
_NoRefactorScreenLayout
NoRefactorScreen
3. 意味のあるまとまりでWidgetを切り出していく
この調子でいくと、引数がどんどん増えていって途方もない作業になります。
そこで画面の構成している部分をある程度パーツに分けて考えていきたいと思います。
最初はScaffoldのAppBarの内容はLayoutクラスの引数としました。
bodyの内容はパーツに分けて考えて切り出していきます。
3-1. 画面をパーツに分ける
今回は以下の内容で区切って考えていきます。
3-2. ファイル内からそれぞれのパーツで切り出していく
最初にImageSliderViewを切り出していきます。
今回は画像部分と下のプログレスバーに見立てた部分をセットで切り出したいです。
53〜104行目を選択して⌘ + .からColumnでラップ
ColumnでラップしたものをExtract WidgetでImageSliderViewとして切り出す
ここで好みは分かれますが、私はさらに別ファイルに切り出します。
切り出す場所としてわかりやすいようにno_refactorディレクトリ配下にwidgetsディレクトリを作成してその中に切り出します。
そのまま同じファイル内におく、またはプライベートクラスでおく場合があると思います。
その場合は同一ファイル内で使っていることが明確なのと探す手間が省けます。
逆にコード量は増えてしまい、目的の部分を見失う可能性もあります。
ここは個人の考え方やチームの方針にそう必要があります。
ImageSliderViewを選択して⌘ + . でMove to ImageSliderView fileと移動場所を選択
NoRefactorScreenLayoutの引数にfinal Widget imageSlider;を宣言し、ImageSliderViewを直接呼び出している部分を差し替えます。
3-3. 他のパーツも切り出していく
上記の手順と同じように切り出していきます。
前回は最初にColumnでラップしましたが、他のパーツは必要ありません。
61〜88行目のClipRRect()をFriendListTileとしてExtract Widget、別ファイルに移動、変数宣言と差し替え
70〜155行目のRow()をMainActionsとしてExtract Widget、別ファイルに移動、変数宣言と差し替え
77〜111行目の GridView.builder()をSubActionsとしてExtract Widget、別ファイルに移動、変数宣言と差し替え
合わせて
- class _SubActionItemConfig
- List<_SubActionItemConfig> _subActionItemConfigs
上記2点をsub_actions.dartに移動しておく
これでNoRefactorScreenのリファクタリングは終了です。
364行だったコードが→88行になりだいぶすっきりしました。
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:sample_of_dividing_widget_into_manageable_parts/no_refactor/widgets/friend_list_tile.dart';
import 'package:sample_of_dividing_widget_into_manageable_parts/no_refactor/widgets/image_slider_view.dart';
import 'package:sample_of_dividing_widget_into_manageable_parts/no_refactor/widgets/main_actions.dart';
import 'package:sample_of_dividing_widget_into_manageable_parts/no_refactor/widgets/sub_actions.dart';
class NoRefactorScreen extends StatelessWidget {
const NoRefactorScreen({super.key});
@override
Widget build(BuildContext context) {
return _NoRefactorScreenLayout(
appBarTitle: 'イカリング3',
appBarActions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'おわる',
style: TextStyle(color: Colors.white),
),
),
],
imageSlider: const ImageSliderView(),
friendListTile: const FriendListTile(),
mainActions: const MainActions(),
subActions: const SubActions(),
);
}
}
class _NoRefactorScreenLayout extends StatelessWidget {
const _NoRefactorScreenLayout({
required this.appBarTitle,
required this.appBarActions,
required this.imageSlider,
required this.friendListTile,
required this.mainActions,
required this.subActions,
});
final String appBarTitle;
final List<Widget> appBarActions;
final Widget imageSlider;
final Widget friendListTile;
final Widget mainActions;
final Widget subActions;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
appBarTitle,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
automaticallyImplyLeading: false,
backgroundColor: Colors.black,
centerTitle: true,
actions: appBarActions,
),
body: SingleChildScrollView(
child: ColoredBox(
color: const Color(0xFF292E35),
child: Column(
children: [
imageSlider,
const Gap(20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: friendListTile,
),
const Gap(20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: mainActions,
),
const Gap(20),
subActions,
],
),
),
),
);
}
}
4. 切り出したWidgetのリファクタリング
切り出した各Widgetも必要に応じてリファクタリングします。
あくまで必要に応じてとしているのは過剰にリファクタリングしすぎるのは
時間を消費してしまい他の開発に支障をきたすからです。
ベターだけど、マストではない、といった感じです。
- 意味のあるまとまりになった
- ネストが浅くなった
- 重複している部分を共通化した
となれば可読性は上がると言えるでしょう。
わかりやすくした結果、逆にコード量が増えることもあります。
しかし、繰り返しになりますがそのことで可読性が上がれば良いと考えます。
今回で行けばfriend_list_tile.dart
は手をつけていません。
image_slider_view.dart
は2.レイアウトに関する内容を隠蔽する
の手順に沿ってクラス内に切り出しているだけなので説明は割愛します。
本項ではでMainActionsとSubActionsに関するリファクタリングを解説したいと思います。
4-1. MainActionsの中からActionItemを切り出す
MainActionsの13行目をExtract Widgetし、さらに別ファイルに移動します。
そこから引数を以下のように取ります。
- final IconData iconData; ⇒ アイコンのデータ
- final Color iconColor; ⇒ アイコン自体の色
- final double iconWidthRatio; ⇒ アイコンの横幅に対するアスペクト比
- final Color backgroundColor; ⇒ 背景色
- final String title; ⇒ タイトルの文字列
4-2. MainActionsのそれぞれの部分にActionItemを適用する
4-3. SubActionsにActionItemを適用する
終わりに
このほかにもまだまだ細かい部分はあると思いますが、今回は基本的な切り出し方について説明させていただきました。
Widgetの切り出しやファイル移動は簡単に行えます。
切り出す時のまとまりの意味を考えていくと自ずと保守性の高いWidgetになると思います。
この記事が誰かのお役に立てれば幸いです。