🗂️

【Flutter】肥大化したWidgetファイルを part でスマートに整理する方法

2025/03/22に公開
15

はじめに

Flutterで画面を作り込んでいくと、ウィジェットのコードや処理が増えて、ファイルがどんどん肥大化していきます。
build メソッドが長くなると、スクロールが大変になり、保守性も下がってしまいますよね。

そんな時に役立つのが、Dartの part / part of 機能です。
ファイルを複数に分けながらも、プライベートなクラスやメソッドを共有できるのが特徴です。

この記事では、part を使って 大きなWidgetファイルをスッキリ整理する方法 を、具体例とともに紹介します。
「整理したいけど、グローバルに公開したくない…」という悩みを解決したい方におすすめです!

記事の対象者

  • Flutterで画面を作っていて、ウィジェットのコードが肥大化してきた方
  • ウィジェットや処理をファイル分割したいけど、プライベートに保ちたいと考えている方
  • Dartの part / part of の使いどころや具体的な使い方を知りたい方
  • メンテナンスしやすく読みやすいコード構成にしたいと考えている方

記事を執筆時点での筆者の環境

[✓] Flutter (Channel stable, 3.27.1, on macOS 15.1 24B2082 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.96.2)

整理していない例

import 'package:flutter/material.dart';
// インポートだけで10行くらいあると仮定

class HomeScreen extends StatelessWidget {
  const HomeScreen({
    super.key,
  });

  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Column(
          children: [
            Text('Hello World!'),

            // ...

            // 500行くらいのコードがあると仮定
          ],
        ),
      ),
    );
  }

  void _onTap() {
    // 何かの処理が100行くらいあると仮定
  }

  void _onTap2() {
    // 何かの処理が100行くらいあると仮定
  }

  // 全部で1000行くらいのコードがあると仮定
}

画面を構築しているファイルにはビルド内の要素だけでなく、インポート文やボタンをタップした処理など様々なコードを記述しますよね。
そうなってくるとファイルが長くなり、スクロールの頻度が増え、見通しが悪くなっていきます。
そこで取れる対策が2つあります。

1. ウィジェットを意味のあるまとまりに分けて分割する

一般的には意味のある大きなまとまりに区切ってウィジェットを分割していくと思います。
エディターの機能でビルドの中から範囲指定でウィジェットに切り出すことができます。

また、切り出したウィジェットは簡単にファイルとして分割することができます。

ただし、ここで考えたいのは、この切り出したウィジェットはどこで使う想定か? と言うことです。
他の画面でも使う場合はこれでいいのですが、この画面でしか使わない場合はグローバルな宣言にしてしまうと、他ファイルからも呼び出せてしまいます。
この場合、 コードサジェストのノイズになったり間違って使ってしまったり と弊害が出てきます。

では、アンダースコア付きのプライベートなクラスにすればいいのでは?と思うのですが、基本的にはプライベートなクラスは同じファイル内でしか読み込みができないため、別ファイルには定義できません。
そこで登場するのが次の方法です。

2. partpart of を使って、一つのファイルを分割管理する

ここからは、今回の記事の本題である「part / part of を使ったファイル分割方法」について解説します。

この方法では、ファイルは複数に分かれていても、Dartの仕組み上は「1つのファイル」として扱われるのが特徴です。

やり方としては、ウィジェットを意味のあるまとまりで切り出す点は通常のファイル分割と似ていますが、以下のような流れで進めます。

  1. ある程度、1つのファイルにまとめてウィジェットを実装する
  2. 意味のある単位でクラスや関数を切り出す(必要ならアンダースコアでプライベート化)
  3. サブファイルとして分割する
  4. サブファイル内の import 文をすべて削除する
  5. サブファイルの先頭に part of 'メインファイル.dart'; を追加する
  6. メインファイルに part 'サブファイル.dart'; を追加する

全体の構成

screen
├── apple
│   ├── part
│   │   ├── _extension.dart
│   │   ├── _my_button.dart
│   │   └── _my_list_tile.dart
│   └── screen.dart
└── home
    └── screen.dart

今回の例題として apple ディレクトリ配下にメインファイルとして screen.dart をおいています。
サブとなるファイルは part ディレクトリ配下にアンダースコアを接頭辞につけて命名してみました。
ここの命名規則は各自のお好みで大丈夫です。

メインファイル

lib/presentations/screen/apple/screen.dart
import 'package:flutter/material.dart';

// このファイルで使用するものをpartでインポート
part 'part/_extension.dart';
part 'part/_my_button.dart';
part 'part/_my_list_tile.dart';

class AppleScreen extends StatelessWidget {
  const AppleScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Timeline'),
        actions: [
          ElevatedButton(
            onPressed: () => _showExtensionSnackBar(context),
            child: const Text('Button'),
          ),
        ],
      ),
      body: ListView(
        children: [
          const _MyListTile(
            title: 'posts',
            subtitle: 'subtitle',
          ),
          _MyButton(
            onPressed: () => _showExtensionSnackBar(context),
          ),
        ],
      ),
    );
  }
}

import の下に part をつけてサブとしてインポートしたいファイルを記述します。
今回はpartディレクトリ配下にあるので、part/ と言うパスになっていますが、同じディレクトリ内でそのままファイル名で大丈夫です。

プライベートなウィジェットとして切り出したファイル

lib/presentations/screen/apple/part/_my_button.dart
// メインとなるファイルを紐付ける
part of '../screen.dart';

class _MyButton extends StatelessWidget {
  const _MyButton({
    required this.onPressed,
  });
  final void Function() onPressed;
  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: const Text('Button'),
    );
  }
}

行頭に part of でメインファイルと紐づけています。
今回は一つ上のディレクトリ階層にメインファイルをおいているのでパスには ../となっています。

特筆すべきはここにはインポート文が必要ないことです。
このウィジェットであれば import 'package:flutter/material.dart'; が本来必要ですが、これはメインファイルに記述があり、そちらから読み込まれているので必要ないことになっています。
また、例えばこのファイルを編集していて、他のパッケージやファイルのメソッドやウィジェットを使いたくなったとします。
その場合もサブファイルでインポートして呼び出しても インポート文はメインファイル側に自動で記述されます。

このことからもファイルは分かれているが、システム上は一つのファイルとして認識されていることがわかります。

プライベートなextensionも読み込める

lib/presentations/screen/apple/part/_extension.dart
part of '../screen.dart';

extension on AppleScreen {
  void _showExtensionSnackBar(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Tapped on button')),
    );
  }
}

エクステンション名をつけない無名のエクステンション内で定義したメソッドはプライベートメソッドとして定義できます。
私はよくボタンをタップした場合の処理などはよくこのプライベートエクステンションを使って分けて書いています。
こうしたものも本来は同一ファイル内でしか呼び出せないのですが、こちらも part of で紐づけることで別ファイルから呼び出すことができます。

余談: プライベートクラスに無名エクステンションもできる

lib/presentations/screen/apple/part/_my_list_tile.dart
part of '../screen.dart';

class _MyListTile extends StatelessWidget {
  const _MyListTile({
    required this.title,
    required this.subtitle,
  });

  final String title;
  final String subtitle;

  
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(title),
      subtitle: Text(subtitle),
      onTap: () => showMySnackBar(context),
    );
  }
}

extension on _MyListTile {
  void showMySnackBar(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Tapped on post ')),
    );
  }
}

プライベートクラスのメソッドを無名エクステンションで定義することもできます。
この方法であれば _MyListTile 内のUI要素だけでなく処理内容も含めて分割することができます。

終わりに

今回は、Dartの part / part of を使って、肥大化しがちなウィジェットファイルを整理する方法を紹介しました。

ファイルを分けつつも、プライベートなクラスやメソッドを保ったまま管理できるこの仕組みは、特に「その画面だけで使う処理」を安全に分離したい場面でとても役立ちます。

慣れるまでは少し独特な書き方に感じるかもしれませんが、一度使い方を覚えてしまえば、保守性・可読性の高いコード構成がぐっと作りやすくなります。

ただし、すべてのウィジェットや処理を必ず分割すべきというわけではありません。
内容がシンプルな場合や、分けることでかえって構造がわかりにくくなる場合は、ひとつのファイルにまとめておく方が適切なケースもあります

part / part of はあくまで整理のための「選択肢のひとつ」。
状況に応じて柔軟に使い分けていきましょう!

もしこの記事が参考になったら、ぜひ自分のプロジェクトにも取り入れてみてくださいね!

15

Discussion