アプリ アーキテクチャ ガイド

アプリのアーキテクチャは、質の高い Android アプリケーションの基盤です。アーキテクチャを適切に定義することで、スマートフォン、タブレット、折りたたみ式デバイス、ChromeOS デバイス、車載ディスプレイ、XR など、拡大し続ける Android デバイスのエコシステムに対応できる、スケーラブルで保守可能なアプリを作成できます。

アプリの構成

標準的な Android アプリは、サービスコンテンツ プロバイダブロードキャスト レシーバなどのさまざまなアプリ コンポーネントで構成されています。これらのコンポーネントは、アプリ マニフェストで宣言します。

アプリのユーザー インターフェースもコンポーネントです。これまで、UI は複数のアクティビティを使用して構築されていました。ただし、最新のアプリではシングル アクティビティ アーキテクチャが使用されています。単一の Activity は、フラグメントまたは Jetpack Compose のデスティネーションとして実装された画面のコンテナとして機能します。

複数のフォーム ファクタ

アプリは、スマートフォンだけでなく、タブレット、折りたたみ式デバイス、ChromeOS デバイスなど、さまざまなフォーム ファクタで実行できます。アプリは縦向きまたは横向きを想定できません。デバイスの回転や折りたたみ式デバイスの折りたたみ / 展開などの構成変更により、アプリは UI を再コンポーズする必要があり、アプリのデータと状態に影響します。

リソースの制約

モバイル デバイス(大画面デバイスも含む)はリソースに限りがあるため、オペレーティング システムは新たなアプリのリソースを確保するために、アプリプロセスを任意のタイミングで停止する場合があります。

バリエーションのリリース条件

リソースが制約された環境では、アプリのコンポーネントは個別に順不同で起動される可能性があります。また、オペレーティング システムやユーザーによって随時破棄される可能性があります。そのため、アプリのデータや状態をアプリ コンポーネント内に保存しないでください。アプリのコンポーネントは自己完結型で、互いに独立している必要があります。

アーキテクチャに関する一般的な原則

アプリのデータや状態の保存にアプリ コンポーネントを使用できないとなると、アプリをどのように設計すればよいのでしょうか。

Android アプリのサイズが大きくなるにつれ、アプリを拡張できるアーキテクチャを定義することが重要になります。適切に設計されたアプリ アーキテクチャは、アプリの各部分の境界と、各部分が担う役割を定義します。

関心の分離

具体的な原則に沿ってアプリ アーキテクチャを設計します。

最も重要な原則は関心の分離です。すべてのコードを 1 つの Activity または Fragment に記述するのはよくある間違いです。

Activity または Fragment の主な役割は、アプリの UI をホストすることです。Android OS は、画面の回転などのユーザー操作や、メモリ不足などのシステム イベントに応じて、頻繁に破棄と再作成を繰り返しながら、ライフサイクルを制御します。

この一時的な性質により、アプリケーション データや状態の保持には適していません。Activity または Fragment にデータを保存すると、コンポーネントが再作成されたときにデータが失われます。データの永続性を確保し、安定したユーザー エクスペリエンスを提供するため、これらの UI コンポーネントに状態を委ねないでください。

アダプティブ レイアウト

アプリは、デバイスの向きの変更やアプリ ウィンドウのサイズの変更などの構成の変更を適切に処理する必要があります。アダプティブな正規化レイアウトを実装して、さまざまなフォーム ファクタで最適なユーザー エクスペリエンスを提供します。

UI をデータモデルで操作する

もう 1 つの重要な原則は、UI をデータモデルで操作することです(永続モデルをおすすめします)。データモデルはアプリのデータを表し、アプリの UI 要素やその他のコンポーネントから独立しています。つまり、UI とアプリ コンポーネントのライフサイクルには関連付けられませんが、OS がアプリのプロセスをメモリから削除したときは、破棄されます。

永続モデルが望ましい理由として、次の点が挙げられます。

  • Android OS がアプリを破棄してリソースを解放してもデータが失われない。

  • ネットワーク接続が不安定または利用不可の場合でもアプリが動作し続ける。

アプリ アーキテクチャをデータモデル クラスに基づいて構築すると、アプリの堅牢性とテストのしやすさを高めることができます。

信頼できる唯一の情報源

アプリ内で新しいデータ型を定義するときは、信頼できる唯一の情報源(SSOT)を割り当てる必要があります。SSOT はそのデータの「オーナー」であり、SSOT のみがそのデータを変更またはミューテーションできます。そのために、SSOT は不変の型を使用してデータを公開します。SSOT がデータを変更するには、関数を公開するか、他の型が呼び出すことができるイベントを受け取ります。

このパターンには、次のような複数のメリットがあります。

  • 特定のデータ型に対するすべての変更を 1 か所に集約できる
  • 他の型によって改ざんされないようにデータを保護できる
  • データに対する変更が追跡しやすくなり、バグを見つけやすくなる

オフライン ファーストのアプリでは、アプリデータの信頼できる情報源は、通常はデータベースです。場合によっては、ViewModel が信頼できる情報源になります。

単方向データフロー

信頼できる唯一の情報源の原則は、単方向データフロー(UDF)パターンと組み合わせて使用されることがよくあります。UDF では、状態は一方向にのみ流れます(通常は親コンポーネントから子コンポーネントへ)。データを変更するイベントはその反対方向に流れます。

Android では、一般的に状態またはデータは、上位スコープの階層の型から下位スコープの階層の型に流れます。一般的にイベントは、下位スコープの型からトリガーされ、対応するデータ型の SSOT に到達するまで流れます。たとえば、一般的にアプリデータはデータソースから UI に流れます。ボタンの押下などのユーザー イベントは UI から SSOT に流れ、SSOT でアプリデータが変更されて、不変の型で公開されます。

このパターンにより、データの整合性の維持が向上し、間違いの発生が減り、デバッグが簡単になります。つまり、SSOT パターンのすべてのメリットが実現されます。

アーキテクチャに関する一般的な原則を考慮すると、各アプリに少なくとも 2 つのレイヤが必要です。

  • UI レイヤ: アプリデータを画面に表示します
  • データレイヤ: アプリのビジネス ロジックを含み、アプリデータを公開します。

ドメインレイヤというレイヤを追加することで、UI レイヤとデータレイヤの間のやり取りを簡素化でき、再利用できます。

通常のアプリ アーキテクチャでは、UI レイヤはデータレイヤから、または UI レイヤとデータレイヤの間にあるオプションのドメインレイヤから、アプリデータを取得します。
図 1.一般的なアプリ アーキテクチャの図。

最新のアプリ アーキテクチャ

最新の Android アプリ アーキテクチャでは、特に次の手法が使用されています。

  • 適応型レイヤード アーキテクチャ
  • アプリのすべてのレイヤにおける単方向データフロー(UDF)
  • UI の複雑さを管理する状態ホルダーを含む UI レイヤ
  • コルーチンとフロー
  • 依存関係の挿入に関するベスト プラクティス

詳細については、Android アーキテクチャに関する推奨事項をご覧ください。

UI レイヤ

UI レイヤ(またはプレゼンテーション レイヤ)の役割は、アプリデータを画面に表示することです。ユーザー操作(ボタンの押下など)または外部入力(ネットワーク レスポンスなど)によってデータが変更されるたびに、変更を反映するように UI を更新する必要があります。

UI レイヤは、次の 2 種類の構成要素で構成されています。

  • データを画面にレンダリングする UI 要素。これらの要素は、Jetpack Compose 関数を使用して作成し、アダプティブ レイアウトをサポートします。
  • データを保持して UI に公開し、ロジックを処理する状態ホルダー(ViewModel など)
一般的なアーキテクチャでは、UI レイヤの UI 要素は状態ホルダーに依存し、状態ホルダーはデータレイヤまたはオプションのドメインレイヤのいずれかのクラスに依存します。
図 2. アプリ アーキテクチャにおける UI レイヤの役割。

アダプティブ UI の場合、ViewModel オブジェクトなどの状態ホルダーは、さまざまなウィンドウ サイズクラスに適応する UI 状態を公開します。この UI 状態は currentWindowAdaptiveInfo() を使用して導出できます。NavigationSuiteScaffold などのコンポーネントは、この情報を使用して、利用可能な画面スペースに基づいて異なるナビゲーション パターン(NavigationBarNavigationRailNavigationDrawer など)を自動的に切り替えることができます。

詳しくは、UI レイヤのページをご覧ください。

データレイヤ

アプリのデータレイヤには、ビジネス ロジックが含まれています。ビジネス ロジックはアプリに価値をもたらすものであり、アプリがデータを作成、保存、変更する方法を決定するルールで構成されています。

データレイヤは、それぞれが 0 から多数のデータソースを含むことができるリポジトリで構成されています。アプリで処理するデータの種類ごとにリポジトリ クラスを作成する必要があります。たとえば、映画に関するデータであれば MoviesRepository クラス、支払いに関するデータであれば PaymentsRepository クラスを作成します。

一般的なアーキテクチャでは、データレイヤのリポジトリはアプリの他の部分にデータを提供し、データソースに依存します。
図 3. アプリ アーキテクチャにおけるデータレイヤの役割。

リポジトリ クラスは次のことを担います。

  • アプリの他の部分にデータを公開する
  • データの変更を一元管理する
  • 複数のデータソース間の競合を解決する
  • アプリの他の部分からデータソースを抽象化する
  • ビジネス ロジックを含む

各データソース クラスは、ファイル、ネットワーク ソース、ローカル データベースなど、1 つのデータソースのみを処理する役割を担う必要があります。データソース クラスは、データ オペレーションのためにアプリとシステムの橋渡しをします。

詳しくは、データレイヤのページをご覧ください。

ドメインレイヤ

ドメインレイヤは、UI レイヤとデータレイヤの間に位置するオプションのレイヤです。

ドメインレイヤは、複雑なビジネス ロジック、または複数のビューモデルで再利用される単純なビジネス ロジックをカプセル化します。すべてのアプリにこのような要件があるわけではないため、ドメイン レイヤはオプションです。複雑さに対処する場合や再利用性を優先する場合など、必要な場合にのみ使用してください。

含まれている場合、オプションのドメインレイヤは UI レイヤに依存関係を提供し、データレイヤに依存します。
図 4. アプリ アーキテクチャにおけるドメインレイヤの役割。

通常、ドメインレイヤのクラスを「ユースケース」または「インタラクタ」と呼びます。各ユースケースは 1 つの機能を担うべきです。たとえば、複数のビューモデルがタイムゾーンに基づいて適切なメッセージを画面に表示する場合、アプリに GetTimeZoneUseCase クラスを持たせることができます。

詳しくは、ドメインレイヤのページをご覧ください。

コンポーネント間の依存関係を管理する

アプリのクラスは、適切に機能するために他のクラスに依存しています。次のいずれかのデザイン パターンを使用して、特定のクラスの依存関係を収集できます。

  • 依存性の注入(DI): 依存性の注入を利用すると、クラスの依存関係を構築することなく定義できます。ランタイムには、別のクラスがこの依存関係を提供します。
  • サービス ロケータ: サービス ロケータ パターンでは、クラスが依存関係を作成せずに取得できるレジストリが提供されます。

こうしたパターンでは、コードが重複して煩雑になることなく依存関係を明確に管理できるため、コードの拡張が可能になります。また、テスト版と製品版の実装を簡単に切り替えることができます。

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

プログラミングは創造的な活動であり、Android アプリの作成も例外ではありません。問題の解決方法は数多くあります。複数のアクティビティやフラグメント間でデータをやり取りする、リモートデータを取得してオフライン モード用にローカルで永続化するなど、重要なアプリで対処する一般的なシナリオにはさまざまなものがあります。

以下の推奨事項は必須ではありませんが、ほとんどの場合これに沿うことで、コードベースの堅牢性を高め、テストとメンテナンスを容易に実施できるようになります。

アプリのコンポーネントにデータを格納しないでください。

アプリのエントリ ポイント(アクティビティ、サービス、ブロードキャスト レシーバなど)をデータソースとして指定しないでください。エントリ ポイントは、そのエントリ ポイントに関連するデータのサブセットを取得する他のコンポーネントとの調整のみを行う必要があります。各アプリ コンポーネントの生存期間は、ユーザーによるデバイスの操作やシステムの容量によって短くなります。

Android クラスへの依存を減らします。

アプリ コンポーネントは、ContextToast など、Android フレームワーク SDK API に依存する唯一のクラスにする必要があります。アプリ コンポーネントからアプリの他のクラスを抽象化すると、テストがしやすくなり、アプリ内の結合を軽減できます。

アプリのモジュール間の責任の境界を明確に定義します。

ネットワークからデータを読み込むコードを、コードベース内の複数のクラスやパッケージに散在させないでください。同様に、関連のない複数の処理(データ キャッシングとデータ バインディングなど)を同じクラスで定義しないでください。アプリの推奨アーキテクチャに沿うと便利です。

各モジュールからの公開はできるだけ行わないでください。

内部実装の詳細を公開するショートカットを作成しないでください。短期的には時間を少し節約できるかもしれませんが、コードベースが発展するにつれて何倍もの技術的負債を負うことになる可能性があります。

アプリの特別な部分に焦点を当てて、他のアプリとの差別化を図ります。

同じボイラープレート コードを何度も書いてすでにあるものを作り直すのではなく、アプリを特別なものにすることに時間とエネルギーを集中させましょう。繰り返しのボイラープレート コードの記述には Jetpack ライブラリやその他の推奨ライブラリを利用してください。

正規レイアウトとアプリの設計パターンを使用します。

Jetpack Compose ライブラリには、アダプティブ ユーザー インターフェースを構築するための堅牢な API が用意されています。アプリで正規化レイアウトを使用して、複数のフォーム ファクタとディスプレイ サイズでユーザー エクスペリエンスを最適化します。アプリの設計パターンのギャラリーを確認して、ユースケースに最適なレイアウトを選択します。

構成変更の前後で UI の状態を保存します。

アダプティブ レイアウトを設計する際は、ディスプレイのサイズ変更、折りたたみ、向きの変更などの構成変更の前後で UI の状態を保持します。アーキテクチャでは、ユーザーの現在の状態が維持され、シームレスなエクスペリエンスが提供されることを確認する必要があります。

再利用可能で構成可能な UI コンポーネントを設計します。

再利用可能で構成可能な UI コンポーネントを構築して、アダプティブ デザインをサポートします。これにより、大幅なリファクタリングを行わずに、さまざまな画面サイズや姿勢に合わせてコンポーネントを組み合わせたり、再配置したりできます。

アプリの各部分を個別にテストできるようにする方法を検討します。

ネットワークからデータを取得するための明確に定義された API を用意することで、そのデータをローカル データベースに永続化するモジュールを簡単にテストできるようになります。そうせずに、2 つの関数のロジックを 1 か所に混在させたり、ネットワーク用のコードをコードベース全体に分散させたりすると、不可能ではないにしても、テストが極めて困難になります。

型は同時実行ポリシーに関する責任を負います。

ある型が時間のかかるブロック処理を実行している場合、その型は適切なスレッドに計算を移動する責任を負います。この型は、実行している計算の種類と、その計算をどのスレッドで実行する必要があるかを認識します。型はメインセーフである(つまり、ブロックせずにメインスレッドから安全に呼び出せる)ことが必要です。

データの関連性と新鮮さをできる限り維持します。

こうすることで、デバイスがオフライン モードのときでも、ユーザーがアプリの機能を利用できるようになります。すべてのユーザーが常に高速な接続を利用できるわけではなく、たとえ利用できるとしても、混雑した場所では受信不良が起きる可能性があることに留意してください。

アーキテクチャのメリット

優れたアーキテクチャをアプリに実装することは、プロジェクト チームとエンジニアリング チームに次のような多くのメリットをもたらします。

  • アプリ全体の保守性、品質、堅牢性が向上します。
  • アプリのスケーリングが可能になります。より多くの人々とチームが、コードの競合を最小限に抑えながら、同じコードベースで開発に寄与できます。
  • オンボーディングに役立ちます。アーキテクチャによってプロジェクトに一貫性がもたらされるため、新しいメンバーが速やかにチームに適応し、短時間でより効率的に作業できるようになります。
  • テストが容易になります。優れたアーキテクチャでは、一般的にテストしやすいシンプルな型が推奨されます。
  • 適切に定義されたプロセスを使用して、体系的にバグを調査できます。

アーキテクチャへの投資は、ユーザーにも直接的な影響を及ぼします。エンジニアリング チームの生産性が高まることで、アプリの安定性と機能性が向上します。ただし、アーキテクチャの実装には事前の準備時間の投資も必要です。事前に組織内の他の部門に対して実装を正当化するには、こちらのケーススタディが役立ちます。優れたアーキテクチャをアプリに実装した企業の成功事例が紹介されています。

サンプル

以下のサンプルは、優れたアプリ アーキテクチャを実証するものです。