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

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

アプリの構成

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

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

複数のフォーム ファクタ

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

リソースの制約

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

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

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

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

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

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

関心の分離

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

最も重要な原則は関心の分離です。アプリを、明確に定義された責任と境界を持つメソッド、クラス、ファイル、パッケージ、モジュール、レイヤに分離します。

すべてのコードを 1 つの Activity に記述するのはよくある間違いです。

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

この一時的な性質により、アプリケーションのデータや状態の保持には適していません。Activity にデータを保存すると、コンポーネントが再作成されたときにデータが失われます。データの永続性を確保し、安定したユーザー エクスペリエンスを提供するため、これらの 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 パターンのすべてのメリットが実現されます。

UDF について詳しくは、Jetpack Compose の単方向データフローをご覧ください。

一般的なアーキテクチャの原則を考慮して、各アプリを少なくとも 2 つのレイヤで設計します。

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

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

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

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

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

  • 適応型で階層化されたアーキテクチャ
  • アプリのすべてのレイヤにおける単方向データフロー(UDF)
  • UI の複雑さを管理する状態ホルダーを含む UI レイヤ
  • コルーチンとフロー
  • 依存関係の挿入に関するベスト プラクティス
  • R8 とベースライン プロファイルを使用したパフォーマンスの最適化と Macrobenchmark を使用した測定

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

UI レイヤ

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

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

  • データを画面にレンダリングする UI 要素。これらの要素は、Jetpack Compose 関数を使用して作成し、アダプティブ レイアウトをサポートします。
  • データを保持して UI に公開し、ロジックを処理する状態ホルダー(ViewModel など)。状態ホルダーは、状態を提供する UI 要素と同じ期間存続する必要があります。たとえば、画面の ViewModel は、画面がアプリのナビゲーション バックスタックから削除されるまでメモリに保持される必要があります。詳細については、状態の有効期間をご覧ください。
一般的なアーキテクチャでは、UI レイヤの UI 要素は状態ホルダーに依存し、状態ホルダーはデータレイヤまたはオプションのドメインレイヤのいずれかのクラスに依存します。
図 2. アプリ アーキテクチャにおける UI レイヤの役割。

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

詳しくは、UI レイヤと Compose UI アーキテクチャをご覧ください。

アダプティブ アプリとナビゲーションについて詳しくは、アダプティブ アプリを作成するとアダプティブ ナビゲーションを作成するをご覧ください。

データレイヤ

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

データレイヤはリポジトリで構成されており、各リポジトリには 0 個以上のデータソースを含めることができます。アプリで処理するデータの種類ごとにリポジトリ クラスを作成します。たとえば、映画に関するデータであれば MoviesRepository クラス、支払いに関するデータであれば PaymentsRepository クラスを作成します。

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

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

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

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

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

ドメインレイヤ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

優れたアーキテクチャには事前の準備時間の投資が必要ですが、ユーザーにも直接的な影響を及ぼします。エンジニアリング チームの生産性が高まることで、アプリの安定性と機能性が向上します。

サンプル

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