一般的なモジュール化パターン

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

すべてのプロジェクトに適合する単独のモジュール化戦略はありません。Gradle は柔軟性が高いため、プロジェクトをまとめる方法について制約はほとんどありません。このページでは、マルチモジュール Android アプリを開発する際に使用できる一般的なルールとパターンの概要について説明します。

高凝集度と低結合度の原則

モジュラー コードベースの特徴として、結合度凝集度のプロパティの使用があげられます。結合度は、モジュールが相互に依存している度合いを示します。凝集度は、この文脈では、1 つのモジュールの要素が機能面でどのように関連しているかを示します。原則として、結合度は低く、凝集度は高くなるようにしてください。

  • 結合度が低いとは、モジュール間の依存度を可能な限り低くすることを指します。これにより、1 つのモジュールに対する変更が他のモジュールに及ぼす影響をゼロまたは最小限にできます。モジュールが他のモジュールの内部動作を認識しないようにする必要があります
  • 凝集度が高いとは、モジュールを構成するコードの集合が 1 つのシステムとして機能することを指します。モジュールには明確に定義された役割が必要で、また特定のドメイン知識の範囲内にある必要があります。サンプルの電子書籍アプリケーションについて考えてみましょう。書籍関連のコードと支払い関連のコードは異なる 2 つの機能ドメインなので、同じモジュール内で併用するのは不適切な場合があります。

モジュールの種類

モジュールを整理する方法は、主にアプリのアーキテクチャによって決まります。推奨されるアプリ アーキテクチャに沿ってアプリに導入できる一般的なモジュールには、次の種類があります。

データ モジュール

データ モジュールには通常、リポジトリ、データソース、モデルクラスが含まれています。データ モジュールの主な役割は次の 3 つです。

  1. 特定のドメインのすべてのデータとビジネス ロジックをカプセル化する: 各データ モジュールは、特定のドメインを表すデータの処理を行います。関連しているものであれば、数多くの種類のデータを処理できます。
  2. リポジトリを外部 API として公開する: データ モジュールの公開 API はアプリの他の部分にデータを公開する役割を担うため、リポジトリでなければなりません。
  3. すべての実装の詳細とデータソースを外部から認識できないようにする: データソースには、同じモジュールのリポジトリしかアクセスできないようにする必要があります。外部からは認識できなくなります。これを実現するには、Kotlin の公開設定キーワード private または internal を使用します。
図 1: サンプルデータ モジュールとその内容

機能モジュール

機能とは、アプリの機能の独立した部分のことで、通常は 1 つの画面または一連の密接に関連する画面(登録フローや決済フローなど)に対応します。アプリにボトムバー ナビゲーションがある場合、それぞれのリンク先は機能であることが多いです。

図 2: このアプリケーションの各タブは機能として定義可能

機能は、アプリの画面やリンク先に関連付けられます。そのため、多くの場合、ロジックと状態を処理するために関連付けられた UI と ViewModel があります。どの機能も、1 つのビューまたはナビゲーションのリンク先には限定されません。機能モジュールはデータ モジュールに依存します。

図 3: 機能モジュールとその内容のサンプル

アプリ モジュール

アプリ モジュールは、アプリケーションのエントリ ポイントです。これらは機能モジュールに依存し、通常はルート ナビゲーションに使用されます。ビルド バリアントにより、1 つのアプリ モジュールをさまざまなバイナリにコンパイルできます。

図 3 : プロダクト フレーバー モジュールの「デモ用」および「完全」依存関係グラフ

アプリが複数のデバイスタイプ(自動車、Wear、テレビなど)をターゲットとする場合は、それぞれについてアプリ モジュールを定義することもできます。これにより、プラットフォーム固有の依存関係を分離できます。

図 4: Wear アプリの依存関係グラフ

共通モジュール

共通モジュール(コアモジュールとも呼ばれます)には、他のモジュールが頻繁に使用するコードが含まれています。これにより冗長性が低くなります。また、これらのモジュールがアプリのアーキテクチャの特定のレイヤを表すことはありません。共通モジュールの例を以下に示します。

  • UI モジュール: アプリ内でカスタム UI 要素や精密なブランディングを使用する場合は、ウィジェット コレクションをカプセル化して、すべての機能を再利用できるようにモジュール化することを検討してください。これにより、複数の機能間で UI の一貫性を保つことができます。たとえば、テーマが一元管理されている場合、リブランディング時に手間のかかるリファクタリングを回避できます。
  • アナリティクス モジュール: トラッキングは、ビジネス要件によって決まルことが多く、ソフトウェア アーキテクチャについて考慮する必要はほぼありません。関連のない多くのコンポーネントでは、多くの場合、アナリティクス トラッカーが使用されます。そのような場合は、専用のアナリティクス モジュールを用意することをおすすめします。
  • ネットワーク モジュール: 多くのモジュールでネットワーク接続を必要とする場合は、HTTP クライアントの提供に特化したモジュールの用意を検討してください。これは、クライアントがカスタム設定を必要とする場合に特に便利です。
  • ユーティリティ モジュール: ユーティリティ(ヘルパーとも呼ばれます)は通常、アプリ全体で再利用される小規模のコードです。ユーティリティの例としては、テストヘルパー、通貨フォーマット関数、メール検証ツール、カスタム演算子などがあります。

モジュール間の通信

モジュールが完全に分離されていることはまれで、多くの場合、他のモジュールに依存し、他のモジュールと通信しています。モジュールが連携して頻繁に情報を交換する場合でも、結合度を低く抑えることが重要です。2 つのモジュール間の直接通信は、アーキテクチャの制約の場合のように、望ましくない場合があります。また、循環依存関係の場合など、不可能な場合もあります。

図 5: モジュール間の直接双方向通信が循環依存関係のため不可能メディエーション モジュールは、他の 2 つの独立したモジュール間のデータフローを調整する場合に必要です。

この問題を解決するには、3 つ目のモジュールで他の 2 つのモジュールをメディエーションします。メディエータ モジュールは、両方のモジュールからのメッセージをリッスンし、必要に応じて転送できます。このサンプルアプリでは、購入手続き画面で購入対象の書籍を認識する必要があります。これは別の機能に含まれる別の画面でイベントが発生した場合でも同様です。この場合、メディエータは、ナビゲーション グラフを所有するモジュール(通常はアプリ モジュール)です。この例では、ナビゲーションを使用し、Navigation コンポーネントによりホーム機能から決済機能にデータを渡します。

navController.navigate("checkout/$bookId")

決済先は、書籍 ID を引数として受け取り、これを使用して書籍に関する情報を取得します。デスティネーション機能の ViewModel 内のナビゲーション引数を取得するには、保存済み状態ハンドルを使用します。

class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      …
}

ナビゲーション引数としてオブジェクトを渡さないようにしてください。代わりに、データレイヤーから目的のリソースにアクセスして読み込むために機能で使用されるシンプルな ID を使用します。これにより、結合度を低く抑えることができ、「信頼できる唯一の情報源」の原則を遵守できます。

以下の例では、両方の機能モジュールが同じデータ モジュールに依存しています。これにより、メディエータ モジュールが転送する必要があるデータ量を最小限に抑え、モジュール間の結合度を低く抑えることができます。モジュールは、オブジェクトを渡す代わりに、プリミティブ ID を交換し、共有データ モジュールからリソースを読み込みます。

図 6: 共有データ モジュールに依存する 2 つの機能モジュール

一般的なベスト プラクティス

冒頭で述べたように、マルチモジュール アプリを開発する方法として唯一の正解はありません。多くのソフトウェア アーキテクチャと同様に、アプリをモジュール化する方法が数多く存在します。それでも、以下の推奨事項に従うことで、コードの可読性、保守性、テストの容易性が向上します。

設定の一貫性を維持する

どのモジュールでも、設定オーバーヘッドが発生します。モジュール数が一定のしきい値に達すると、一貫した設定の管理が難しくなります。たとえば、各モジュールで同じバージョンの依存関係を使用することが重要です。依存関係のバージョンを上げるためだけに多数のモジュールを更新する必要がある場合、作業量が増えるだけでなく、間違いが生じる可能性も高くなります。この問題を解決するには、Gradle のいずれかのツールを使用して設定を一元化します。

  • バージョン カタログ: 同期中に Gradle によって生成される依存関係の型安全なリストです。これは、すべての依存関係を 1 か所で宣言できるようにするための場所で、プロジェクト内のすべてのモジュールが使用できます。
  • コンベンション プラグイン: モジュール間でビルドロジックを共有します。

公開範囲を可能な限り限定する

モジュールの公開インターフェースは最小限にとどめ、必要なものしか公開しないようにする必要があります。実装の詳細が外部に漏洩しないようにしなければなりません。すべての内容について、可能な限り範囲を限定します。Kotlin の公開設定スコープ private または internal を使用して、宣言をモジュール プライベートにします。モジュールで依存関係を宣言する場合は、api よりも implementation を優先して使用してください。前者の場合、モジュールを使用する側に対して、依存関係の推移が公開されます。implementation を使用すると、再ビルドが必要なモジュールの数が少なくなるため、ビルド時間が短縮されます。

Kotlin モジュールと Java モジュールを優先する

Android Studio がサポートするモジュールには、次の 3 つの重要なタイプがあります。

  • アプリ モジュール: アプリケーションのエントリ ポイントです。ソースコード、リソース、アセット、AndroidManifest.xml を含めることができます。アプリ モジュールの出力は、Android App Bundle(AAB)または Android Application Package(APK)です。
  • ライブラリ モジュール: 内容はアプリ モジュールと同じです。これらは他の Android モジュールで依存関係として使用されています。ライブラリ モジュールの出力は Android Archive(AAR)で、構造的にはアプリ モジュールと同一になりますが、Android Archive(AAR)ファイルにコンパイルされ、後で他のモジュールで依存関係として使用できます。ライブラリ モジュールを使用すると、多くのアプリ モジュールで同じロジックとリソースをカプセル化して再利用できます。
  • Kotlin および Java のライブラリ: Android のリソース、アセット、マニフェスト ファイルは含まれていません。

Android モジュールにはオーバーヘッドが伴うため、できるだけ Kotlin または Java を使用することをおすすめします。