Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】🧱 Breaking the Monolith: A Practical, Step-by-Step Guide to Modularizing Your Android App - Part 1

2025年09月20日に「Dev.to」が公開したITニュース「🧱 Breaking the Monolith: A Practical, Step-by-Step Guide to Modularizing Your Android App - Part 1」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

大規模Androidアプリのモジュール化は、ビルドを高速化し、チーム開発を効率化する。この記事では、アプリの設計思想、GradleのConvention Pluginsでビルド設定を共通化する方法、具体的な機能モジュールの作り方を段階的に解説する。

ITニュース解説

Androidアプリ開発において、特に大規模なプロダクトでは、アプリの「モジュール化」が非常に重要な課題となる。モノリスと呼ばれる、すべてのコードが一つにまとまったアプリでは、開発が進むにつれて様々な問題が発生する。

モノリスなアプリとは、文字通り巨大な一枚岩のように、すべての機能やロジックが単一のモジュール、つまり単一の大きなコードベースに集約された状態を指す。これは、アプリの初期段階や小規模なプロジェクト、学習用アプリには適しており、シンプルに開発を始められる利点がある。しかし、アプリの規模が大きくなり、機能が増え、開発チームの人数が増えると、この単一モジュール構造がボトルネックとなる。例えば、コードの一部を変更するだけでアプリ全体を再コンパイルする必要があり、ビルド時間が大幅に伸びてしまう。また、複数の開発者が同じコードベースを触ることで、コードの競合(マージコンフリクト)が頻繁に発生し、チームの開発速度が低下する。さらに、UI、ビジネスロジック、データアクセスといった異なる役割のコードが混在することで、コードの境界が曖昧になり、保守が困難になるだけでなく、将来的な機能拡張やテストも難しくなる。

このような問題を解決するために、アプリを機能や役割ごとに小さな「モジュール」に分割する「モジュール化」が必要となる。モジュール化の主な利点は多岐にわたる。まず、ビルドパフォーマンスが向上する。変更があったモジュールだけを再コンパイルすればよくなるため、ローカルでのビルドや継続的インテグレーション(CI)の時間が大幅に短縮される。次に、チームの作業効率が高まる。各チームが独立したモジュールを担当できるため、並行して作業を進められ、コードの競合も減少する。さらに、各モジュールの境界が明確になることで、UI、ドメイン(ビジネスロジック)、データといったアーキテクチャの原則が強制され、コード品質が維持されやすくなる。また、共通のデザインシステムやユーティリティなどは安定したライブラリとして再利用できるようになり、開発効率を高める。将来的には、Androidのダイナミック機能配信(Dynamic Feature Delivery)といった高度な機能導入への道も開かれる。ただし、小規模なアプリや個人プロジェクト、プロトタイプなどでは、モジュール化によるオーバーヘッドがメリットを上回る場合もあるため、常にモジュール化が最適とは限らない。

モジュール化を進めるにあたっては、明確な目標設定が重要だ。最終的なモジュールの構成として、主に以下のものが考えられる。アプリのエントリポイントとなる:appモジュールがあり、これに加えて、ビジネスロジックの中核を担うモデルやユースケース、リポジトリのインターフェースを定義する純粋なKotlinモジュールである:core-domain、データソースの実装やDTO(データ転送オブジェクト)、マッパー、リポジトリの実装、そしてHiltのDI設定などを含む:core-dataがある。さらに、共通のUIコンポーネントやテーマをまとめた:core-ui、ナビゲーションのルート契約やディープリンク定数を管理する:core-navigation、汎用的なユーティリティやベースクラスを含む:core-commonといったコアモジュール群を設ける。そして、各機能(例: ホーム、詳細、プロフィール、ブックマーク)ごとにUI、ViewModel、ナビゲーショングラフの定義を含む:feature-home:feature-detail:feature-profile:feature-bookmarkのような機能モジュールを作成する。これらのモジュールは、原則として機能モジュールがコアモジュールに依存し、機能モジュール同士は直接依存しない構造を目指す。:appモジュールは、これらすべてのモジュールを統合し、アプリ全体を構築する役割を担う。DI(Dependency Injection)のHilt設定は、独立したモジュールにせず、関連する実装が配置されているモジュールに含めるのが一般的だ。

コードに手を加える前に、綿密な計画を立てることが不可欠である。まず、「ドメインを最初に安定させる」ことが重要だ。これは、アプリの核となるビジネスロジック(例:ユーザー、画像などのモデルや、リポジトリのインターフェース)を:core-domainモジュールに抽出し、Androidに依存しない純粋なKotlinで定義することを意味する。ドメインはアプリの「公開API」のようなものであり、これを最初に安定させることで、他のすべてのモジュールが安定した契約に依存できるようになり、後の変更による影響を最小限に抑え、ビルド速度とテスト容易性を向上させる。

次に、「最も再利用されるものを抽出する」。アプリ全体で共通して使われるデザインシステム(色、文字スタイル、テーマ)、再利用可能なUIコンポーネント(例:共通のトップバー、エラー表示)、そしてナビゲーションの定数などをそれぞれ:core-ui:core-navigationに抽出する。これらを早期に分離することで、各機能モジュールが共通の基盤を利用できるようになり、重複を防ぎ、モジュール間の結合度を低減できる。

さらに、「安全な移行順序を選ぶ」ことも重要だ。すべてのコードを一気に分割するのではなく、リスクの低い、依存関係が少ない機能から順にモジュール化を進める。例えば、ブックマーク機能のような単純な画面から始め、次に詳細画面、そして複数のネストされたグラフを持つプロフィール画面、最後に多くの機能と依存関係を持つホーム画面といった順序で進める。これにより、新しいモジュール構成のセットアップを段階的に検証し、リスクを管理しながら進行できる。

「ナビゲーションの境界を明確にする」も重要な原則だ。各機能モジュールは自身のナビゲーショングラフを定義するが、ルートの定数などは:core-navigationに集約する。そして、:appモジュールだけがこれらすべてのナビゲーションを結合する役割を担う。これにより、機能モジュールは互いの内部を知ることなく、共有された契約に基づいて連携できるようになる。

最後に、「DI(Dependency Injection)の境界を準備する」。インターフェースは:core-domainに、それらの実装とHiltによる依存性バインディングは:core-dataに配置する。機能モジュールは、具体的な実装ではなく、インターフェースにのみ依存するようにする。これは「依存性逆転の原則」に則ったもので、モジュール間の依存関係をクリーンに保ち、テストや将来的な実装の変更を容易にする。

モジュール化に着手する前には、いくつかの「事前準備チェックリスト」をこなしておく必要がある。まず、GradleのVersion Catalog(libs.versions.tomlファイル)を作成し、Compose、Kotlin、Hilt、Retrofit、Roomなど、すべての依存関係のバージョンを一元管理する。これにより、複数のモジュール間でバージョンの一貫性を保ち、更新作業を簡素化できる。次に、「Convention Plugins」またはbuildSrcを導入し、compileSdk、Composeの設定、Kotlinオプションなど、共通のGradle設定を抽出する。これにより、新しいモジュールを追加する際の定型コードを減らし、設定の一貫性を確保する。さらに、共有するリソース(色、文字列、ドローアブルなど)にはcore_のようなプレフィックスを付ける「リソース衛生」を徹底し、将来的な名前空間の衝突を防ぐ。依存関係のスコープは、デフォルトでimplementationを使用し、コンシューマが型を参照する必要がある場合にのみapiを使用することで、カプセル化を強化する。KSPの利用やGradleビルドキャッシュの有効化など、ビルド性能最適化も忘れずに行う。テスト戦略としては、各モジュール内にテストを配置し、CIで変更されたモジュールのみのテストを実行するよう設定する。これらの準備を行うことで、その後のモジュール移行作業がはるかにスムーズで予測可能なものになる。

共通のGradleビルドロジックを管理する方法として、「Convention Plugins」とbuildSrcの二つの選択肢があるが、一般的にはConvention Pluginsが推奨される。Convention Pluginsは、KotlinやGroovyで書かれたコードの集合体で、特定のGradleプロジェクト(モジュール)に事前定義された設定群を適用する。例えば、一般的なAndroidライブラリモジュールが必要とする設定をまとめたandroid-library-convention.gradle.ktsのようなプラグインを作成できる。Convention Pluginsは、buildSrcとは異なり、メインのプロジェクトのクラスパスとは独立した「インクルードビルド」として扱われるため、ビルドロジックの変更がメインプロジェクトの完全な再コンパイルを引き起こしにくい。これにより、ビルドロジックがアプリケーションのソースコードから明確に分離され、スケーラビリティとテスト容易性が向上する。GoogleのAndroidXやNow in Androidプロジェクトでも大規模なモジュール化のために採用されている方法だ。

Convention Pluginの実装は、まずプロジェクトのルートにconvention-pluginsディレクトリを作成し、その中にsettings.gradle.ktsbuild.gradle.ktsを配置して、これを独立したGradleビルドとして初期化する。このsettings.gradle.kts内で、ルートプロジェクトのlibs.versions.tomlを参照できるように設定する。次に、ルートのsettings.gradle.ktsincludeBuild("convention-plugins")を追加し、このプラグインビルドをメインプロジェクトに組み込む。

カスタムGradleプラグインを定義する際には、まず具体的な機能モジュール(例:ブックマーク)を一つ取り出し、そのbuild.gradle.ktsファイルから、どの設定が他のAndroidライブラリモジュールでも共通して使われるかを特定する。例えば、com.android.libraryorg.jetbrains.kotlin.androidプラグインの適用、compileSdkminSdktargetSdktestInstrumentationRunnerなどのSDKバージョン設定、Javaバージョン互換性、共通のテストライブラリの依存関係などだ。これらの共通設定をまとめたAndroidLibraryConventionPlugin.ktというファイルを作成し、convention-pluginsビルドのsrc/main/kotlin配下に配置する。このプラグインは、KotlinとComposeに関する設定(kotlinCompilerExtensionVersionjvmTargetなど)も一元的に管理する。作成したプラグインは、convention-plugins/build.gradle.kts内でID(例:pinterest.android-library-convention)と実装クラス名を指定して登録する。

このカスタムプラグインを活用して、実際に:feature-bookmarksモジュールを実装する。まず、feature-bookmarksというAndroidライブラリモジュールをプロジェクト内に作成し、モノリスな:appモジュールからブックマーク機能に関連するコード(UI、ViewModel、ナビゲーション定義、リソースなど)を移動する。この際、最も重要なのは「循環依存」を避けることだ。例えば、ブックマーク画面が:appモジュールのAppViewModelに直接依存している場合、:feature-bookmarks:appに依存してしまうため、これは避けなければならない。代わりに、ブックマーク画面が「トップバーの設定ニーズを記述し、その設定を呼び出し元(:appモジュール)が満たす」ような、依存性逆転のパターンにリファクタリングする。具体的には、ブックマーク画面自身が使用する文字列リソースを自身のモジュール内に定義し、BookmarkNavGraph.kt:appのViewModelに依存しないように修正する。

コードの移動とリファクタリングが完了したら、:feature-bookmarksモジュールのbuild.gradle.ktsを確認する。ここには、先ほど特定した共通設定のボイラープレートが多数記述されているはずだ。これを、作成したAndroidLibraryConventionPluginを適用するように変更する。具体的には、pluginsブロックでid("pinterest.android-library-convention")を記述し、androidブロックにはモジュール固有のnamespaceのみを残し、dependenciesブロックにはこのモジュールに「固有の」依存関係のみを記述する。これにより、build.gradle.ktsファイルは非常に簡潔になり、共通設定は一箇所で管理されるため、メンテナンス性が格段に向上する。Gradleファイルを同期し、アプリを実行して、ブックマーク画面に問題なくナビゲートできることを確認すれば、最初の機能モジュールのモジュール化とConvention Pluginの適用が成功したことになる。

この段階でのモジュール間の相互作用は次のようになる。libs.versions.tomlAndroidLibraryConventionPlugin.ktがビルドロジックと設定の基盤を提供する。:feature-bookmarks/build.gradle.ktsは、このConvention Pluginを適用することで、モジュールのビルド方法を定義する。:app/build.gradle.ktsも同様に自身のビルドを定義する。Gradleのビルドプロセス中に、:feature-bookmarksモジュールのコードは.aar(Android Archive)形式のライブラリにコンパイルされる。そして、:appモジュールはこの:feature-bookmarks.aarを依存関係として含み、最終的にアプリ全体が.apk(Android Package)ファイルとしてパッケージ化される。アプリがデバイス上で実行される際、ユーザーがメインUIからブックマーク機能にアクセスしようとすると、:appモジュール内のランタイムが:feature-bookmarksモジュールで定義されたブックマーク機能のランタイムを呼び出し、そのUIと機能が提供される。

このように、モジュール化は段階的に、計画的に進めることで、アプリのビルドパフォーマンス、開発効率、コードの保守性を大幅に改善できる。最初の機能モジュールを成功裏に分離し、Convention Pluginsを使ってビルドロジックを一元管理できたことは、大規模なアプリ開発における重要な一歩となる。

関連コンテンツ

関連IT用語