LoginSignup
2
0

【Flutter】Widgetを管理しやすいように分割する

Posted at

はじめに

学習当初に悩んでいたことで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.dartScaffoldを選択して「⌘ + .」(コマンド + ドット)
からExtract Widgetを選択してWidgetとして切り出します。

スクリーンショット 2024-05-03 18.26.47.png

このクラスはこのファイル内でしか使わないことが確定しているのでアンダースコアをつけて
プライベートなクラスで宣言します。
命名は_NoRefactorScreenLayoutとします

スクリーンショット 2024-05-03 18.28.11.png

2-3. レイアウト関連ではないものを引数にする:title編

ここでは上から順番に整理していきます。
最初にAppBarの部分です。
titleで表示している文字列’イカリング3’を引数にします。

変数final String appBarTitle;で定義

スクリーンショット 2024-05-03 18.40.03.png

コストラクターにも名前付き引数で宣言

スクリーンショット 2024-05-03 18.40.55.png

この時、super.keyは現在使う予定がないので消しておいて大丈夫

スクリーンショット 2024-05-03 18.41.39.png

_RefactorScreenLayoutの’イカリング3’の部分をappBarTitleに置き換え
(constの付け直しも忘れずに)

スクリーンショット 2024-05-03 18.43.48.png

RefactorScreen のビルド関数内で呼び出している_RefactorScreenLayoutの引数を追加

スクリーンショット 2024-05-03 18.45.23.png

引数に’イカリング3’を追記

スクリーンショット 2024-05-03 18.46.03.png

差分は以下

スクリーンショット 2024-05-04 12.02.58.png

2-4. レイアウト関連ではないものを引数にする:actions編

次にactionsに入っているおわるボタンを変更していきます。
actionsは元々Widgetを配列で渡せるようになっています。
今後ここに他のWidgetが追加になるかもしれないことも考えて、ここの引数はWidgetの配列で渡すようにします。

先ほどの要領でやっていく結果は以下のようになります。

_NoRefactorScreenLayout

スクリーンショット 2024-05-04 12.16.28.png

NoRefactorScreen

スクリーンショット 2024-05-04 12.17.07.png

3. 意味のあるまとまりでWidgetを切り出していく

この調子でいくと、引数がどんどん増えていって途方もない作業になります。
そこで画面の構成している部分をある程度パーツに分けて考えていきたいと思います。

最初はScaffoldのAppBarの内容はLayoutクラスの引数としました。
bodyの内容はパーツに分けて考えて切り出していきます。

3-1. 画面をパーツに分ける

今回は以下の内容で区切って考えていきます。

【Flutter】Widgetを管理しやすいように分割する.001.png

3-2. ファイル内からそれぞれのパーツで切り出していく

最初にImageSliderViewを切り出していきます。
今回は画像部分と下のプログレスバーに見立てた部分をセットで切り出したいです。

53〜104行目を選択して⌘ + .からColumnでラップ
スクリーンショット 2024-05-04 12.57.59.png

ColumnでラップしたものをExtract WidgetでImageSliderViewとして切り出す
スクリーンショット 2024-05-04 13.00.41.png

ここで好みは分かれますが、私はさらに別ファイルに切り出します。

切り出す場所としてわかりやすいようにno_refactorディレクトリ配下にwidgetsディレクトリを作成してその中に切り出します。

そのまま同じファイル内におく、またはプライベートクラスでおく場合があると思います。
その場合は同一ファイル内で使っていることが明確なのと探す手間が省けます。
逆にコード量は増えてしまい、目的の部分を見失う可能性もあります。

ここは個人の考え方やチームの方針にそう必要があります。

ImageSliderViewを選択して⌘ + . でMove to ImageSliderView fileと移動場所を選択
スクリーンショット 2024-05-04 13.02.02.png

変更後
スクリーンショット 2024-05-04 13.18.11.png

NoRefactorScreenLayoutの引数にfinal Widget imageSlider;を宣言し、ImageSliderViewを直接呼び出している部分を差し替えます。

_NoRefactorScreenLayout
スクリーンショット 2024-05-04 13.22.01.png

NoRefactorScreen
スクリーンショット 2024-05-04 13.21.38.png

3-3. 他のパーツも切り出していく

上記の手順と同じように切り出していきます。
前回は最初にColumnでラップしましたが、他のパーツは必要ありません。

61〜88行目のClipRRect()をFriendListTileとしてExtract Widget、別ファイルに移動、変数宣言と差し替え
スクリーンショット 2024-05-04 13.55.10.png

70〜155行目のRow()をMainActionsとしてExtract Widget、別ファイルに移動、変数宣言と差し替え
スクリーンショット 2024-05-04 14.03.56.png

77〜111行目の GridView.builder()をSubActionsとしてExtract Widget、別ファイルに移動、変数宣言と差し替え
スクリーンショット 2024-05-04 14.11.59.png

合わせて

  • class _SubActionItemConfig
  • List<_SubActionItemConfig> _subActionItemConfigs

上記2点をsub_actions.dartに移動しておく

これでNoRefactorScreenのリファクタリングは終了です。
364行だったコードが→88行になりだいぶすっきりしました。

no_refactor_screen.dart
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.dart2.レイアウトに関する内容を隠蔽する
の手順に沿ってクラス内に切り出しているだけなので説明は割愛します。

本項ではでMainActionsとSubActionsに関するリファクタリングを解説したいと思います。

【Flutter】Widgetを管理しやすいように分割する.002.png

4-1. MainActionsの中からActionItemを切り出す

MainActionsの13行目をExtract Widgetし、さらに別ファイルに移動します。

そこから引数を以下のように取ります。

  • final IconData iconData; ⇒ アイコンのデータ
  • final Color iconColor; ⇒ アイコン自体の色
  • final double iconWidthRatio; ⇒ アイコンの横幅に対するアスペクト比
  • final Color backgroundColor; ⇒ 背景色
  • final String title; ⇒ タイトルの文字列

スクリーンショット 2024-05-06 10.22.27.png

4-2. MainActionsのそれぞれの部分にActionItemを適用する

スクリーンショット 2024-05-06 10.33.58.png

4-3. SubActionsにActionItemを適用する

スクリーンショット 2024-05-06 10.38.19.png

終わりに

このほかにもまだまだ細かい部分はあると思いますが、今回は基本的な切り出し方について説明させていただきました。
Widgetの切り出しやファイル移動は簡単に行えます。
切り出す時のまとまりの意味を考えていくと自ずと保守性の高いWidgetになると思います。

この記事が誰かのお役に立てれば幸いです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0