🍣

Flutterにおけるウィジェット分割の選択肢(メソッド or クラス)

に公開

はじめに

UIを構築する際などファイルが肥大化しない様に整理する方法として、

  • メソッドによる分割
  • クラスによる分割

があります。

今まで何となくな感じでぼんやりと使ってました。
ちゃんと理解しようと思ったので、それぞれのアプローチの特徴やメリット・デメリット、そして適切な使い分け方についてメモ的な感じでまとめたいと思います。

1. メソッドによる分割

メソッドによる分割は、単一のクラス内で複数のメソッドを使用してUIの各部分を構築しています。

よくありそうな商品詳細画面のサンプルコードを例にします。

  • 実装例
class ProductDetailScreen extends StatelessWidget {
  final Product product;

  const ProductDetailScreen({required this.product, Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('商品詳細')),
      body: Column(
        children: [
          _buildProductHeader(),
          _buildProductInfo(),
          _buildPurchaseOptions(),
        ],
      ),
    );
  }

  Widget _buildProductHeader() {
    return Container(
      height: 250,
      width: double.infinity,
      child: Image.network(
        product.imageUrl,
        fit: BoxFit.cover,
      ),
    );
  }

  Widget _buildProductInfo() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            product.name,
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const Gap(8),
          Text(
            ${product.price}',
            style: const TextStyle(fontSize: 20, color: Colors.red),
          ),
          const Gap(16),
          Text(product.description),
        ],
      ),
    );
  }

  Widget _buildPurchaseOptions() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton.icon(
            onPressed: () {},
            icon: const Icon(Icons.shopping_cart),
            label: const Text('カートに追加'),
          ),
          const Gap(16),
          OutlinedButton.icon(
            onPressed: () {},
            icon: const Icon(Icons.favorite_border),
            label: const Text('お気に入り'),
          ),
        ],
      ),
    );
  }
}

メリット

  1. 実装がシンプル:追加のクラスを作成せずに済むため、実装が簡単です
  2. 状態やデータの共有が容易:クラス内の変数やメソッドに直接アクセスできます
  3. 1つのファイルに対するコード全体の見通し:関連するコードが1つのファイル内に収まります

デメリット

  1. 再利用性の欠如:プライベートメソッドはクラス外から呼び出せないため、他の画面で再利用できない
  2. テスト困難性:プライベートメソッドは単体テストが難しい?らしい
  3. クラスの肥大化:実装が複雑になったり機能追加などがあるとクラスが肥大化する。

2. クラスによる分割

クラスによる分割は、UIの各部分を独立したウィジェットクラスとして実装し、それらを組み合わせて画面を構築しています。

  • 実装例
class ProductDetailScreen extends StatelessWidget {
  final Product product;

  const ProductDetailScreen({required this.product, Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('商品詳細')),
      body: Column(
        children: [
          _ProductHeader(product: product),
          _ProductInfo(product: product),
          _PurchaseOptions(productId: product.id),
        ],
      ),
    );
  }
}

class _ProductHeader extends StatelessWidget {
  final Product product;

  const _ProductHeader({required this.product, Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      height: 250,
      width: double.infinity,
      child: Image.network(
        product.imageUrl,
        fit: BoxFit.cover,
      ),
    );
  }
}

class _ProductInfo extends StatelessWidget {
  final Product product;

  const _ProductInfo({required this.product, Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            product.name,
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const Gap(8),
          Text(
            ${product.price}',
            style: const TextStyle(fontSize: 20, color: Colors.red),
          ),
          const Gap(16),
          Text(product.description),
        ],
      ),
    );
  }
}

class _PurchaseOptions extends StatelessWidget {
  final String productId;

  const _PurchaseOptions({required this.productId, Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton.icon(
            onPressed: () {},
            icon: const Icon(Icons.shopping_cart),
            label: const Text('カートに追加'),
          ),
          const Gap(16),
          OutlinedButton.icon(
            onPressed: () {},
            icon: const Icon(Icons.favorite_border),
            label: const Text('お気に入り'),
          ),
        ],
      ),
    );
  }
}

メリット

  1. 再利用性:独立したウィジェットは他の画面でも再利用できます
  2. 責任の分離:各ウィジェットが特定の役割に集中できます
  3. コードの可読性:各ウィジェットの役割が明確になります

デメリット

  1. ボイラープレートコードの増加:新しいクラスごとに定型コードが必要になる(ここが個人的にネックでした)
  2. データの受け渡しが煩雑:親から子へのデータ渡しが明示的に必要
  3. ファイル数の増加:多くのウィジェットを別ファイルに分けると、どうしてもファイル数が増える

使い分け

条件 メソッド クラス
実装のシンプルさ クラス毎にボイラーブレードコード(お決まりコード)が増える
再利用性 再利用の必要なし 複数の画面で再利用する可能性あり
チームサイズ 小規模チーム/個人開発 大規模チーム
状態管理 単一の状態で十分 複数の独立した状態が必要
パフォーマンス要件 低〜中程度 高い(部分的な再ビルドが重要)
constの活用 メソッド内の個別ウィジェットのみ ウィジェットクラス全体をconstにできる
メモリ効率 標準 高い(constウィジェットによるメモリ共有)

まとめ

メソッドによる分割とクラスによる分割も、それぞれに長所と短所があります。

シンプルに、所属している会社(PJ)のルールに従うのが一番良いですが、個人開発とかまだその辺ルールが決まってない初期スタートの開発ではプロダクトの規模感やスケールで決めるのが良いかもしれません。あと将来的な拡張性とか?

まとめると

  • メソッドによる分割は、シンプルさと開発速度を重視しつつ、比較的小規模なプロダクトの場合に適している。
  • クラスによる分割は、再利用性、テスタビリティ、保守性を重視する場合に適している。(中規模以上のプロダクト)

って感じかなと。
間違いがあれば指摘してください。

Discussion