このガイドでは、品質の高い堅牢なアプリを作成するためのおすすめの方法と推奨アーキテクチャを紹介します。
モバイルアプリのユーザー エクスペリエンス
標準的な Android アプリは、さまざまなアプリ コンポーネント(アクティビティ、フラグメント、サービス、コンテンツ プロバイダ、ブロードキャスト レシーバなど)で構成されています。これらのアプリ コンポーネントのほとんどを、アプリ マニフェストで宣言しておきます。Android OS ではこのアプリ マニフェスト ファイルを基に、デバイスの全体的なユーザー エクスペリエンスにアプリをどのように組み込むかを決定します。一般的な Android アプリには複数のコンポーネントが含まれており、ユーザーが短時間に複数のアプリを頻繁に操作することを考慮すると、アプリはユーザー主導のさまざまなワークフローとタスクに適応する必要があります。
また、モバイル デバイスはリソースに限りがあるため、オペレーティング システムは新たなアプリのリソースを確保するために、アプリプロセスを任意のタイミングで強制終了する場合があります。
このような環境の条件を考慮すると、アプリ コンポーネントは個別に順不同で起動され、オペレーティング システムやユーザーによって随時破棄される可能性があります。デベロッパーはこうしたイベントを制御できないため、アプリのデータや状態をアプリ コンポーネント内に保存したり、メモリ内に保持したりしないでください。また、アプリ コンポーネントは互いに依存してはなりません。
アーキテクチャに関する共通の原則
アプリのデータや状態の保存にアプリ コンポーネントを使用できないとなると、アプリをどのように設計すればよいのでしょうか。
Android アプリのサイズが大きくなるにつれ、アプリを拡張し、堅牢性を高め、アプリをテストしやすくするアーキテクチャを定義することが重要になります。
アプリ アーキテクチャは、アプリの各部分の境界と、各部分が担う役割を定義します。前述のニーズを満たすには、具体的な原則に沿ってアプリ アーキテクチャを設計する必要があります。
関心の分離
最も重要な原則は関心の分離です。すべてのコードを 1 つの Activity
または Fragment
に記述するのはよくある間違いです。これらの UI ベースのクラスには、UI やオペレーティング システムとのやり取りを処理するロジックのみを含めます。これらのクラスをできる限りシンプルに保つことで、コンポーネントのライフサイクルに関連する多くの問題を回避し、クラスのテストのしやすさを向上させることができます。
Activity
と Fragment
の実装はデベロッパーが管理するものではないことにご注意ください。これらのクラスは、Android OS とアプリの間のコントラクトを体現する単なる結合クラスです。Android OS は、ユーザーの操作に基づいて、またはシステムの状態(メモリ不足など)を理由として、いつでもこれらのクラスを破棄できます。十分なユーザー エクスペリエンスを実現し、アプリを管理しやすくするために、クラスへの依存を最小限に抑えることをおすすめします。
UI をデータモデルで操作する
もう 1 つの重要な原則は、UI をデータモデルで操作することです(永続モデルをおすすめします)。データモデルはアプリのデータを表し、アプリの UI 要素やその他のコンポーネントから独立しています。つまり、UI とアプリ コンポーネントのライフサイクルには関連付けられませんが、OS がアプリのプロセスをメモリから削除することを決定したときは、破棄されます。
永続モデルが望ましい理由として、次の点が挙げられます。
Android OS がアプリを破棄してリソースを解放してもデータが失われない。
ネットワーク接続が不安定または利用不可の場合でもアプリが動作し続ける。
アプリ アーキテクチャをデータモデル クラスに基づいて構築すると、アプリのテストのしやすさと堅牢性を高めることができます。
信頼できる唯一の情報源
アプリ内で新しいデータ型を定義するときは、信頼できる唯一の情報源(SSOT)を割り当てる必要があります。SSOT はそのデータの「オーナー」であり、SSOT のみがそのデータを変更またはミューテーションできます。そのために、SSOT は不変の型を使用してデータを公開します。SSOT がデータを変更するには、関数を公開するか、他の型が呼び出すことができるイベントを受け取ります。
このパターンには、次のようないくつかのメリットがあります。
- 特定のデータ型に対するすべての変更を 1 か所に集約できる。
- 他の型によって改ざんされないようにデータを保護できる。
- データに対する変更が追跡しやすくなり、それによりバグを見つけやすくなる。
オフライン ファーストのアプリでは、アプリデータの信頼できる情報源は、通常はデータベースです。場合によっては、ViewModel や UI が信頼できる情報源になります。
単方向データフロー
単方向データフロー(UDF)パターンに関する Android のガイドでは、信頼できる唯一の情報源の原則がよく使用されます。UDF では、状態は一方向にのみ流れます。データを変更するイベントはその反対方向に流れます。
Android では、一般的に状態またはデータは、上位スコープの階層の型から下位スコープの階層の型に流れます。一般的にイベントは、下位スコープの型からトリガーされ、対応するデータ型の SSOT に到達するまで流れます。たとえば、一般的にアプリデータはデータソースから UI に流れます。ボタンの押下などのユーザー イベントは UI から SSOT に流れ、SSOT でアプリデータが変更されて、不変の型で公開されます。
このパターンにより、データの整合性の保証が向上し、間違いの発生が減り、デバッグが簡単になります。つまり、SSOT パターンのすべてのメリットが実現されます。
アプリの推奨アーキテクチャ
このセクションでは、推奨されるベスト プラクティスに沿ってアプリを構築する方法について説明します。
前のセクションで説明したアーキテクチャに関する一般的な原則を考慮すると、各アプリに少なくとも 2 つのレイヤが必要です。
- 画面にアプリデータを表示する UI レイヤ。
- アプリのビジネス ロジックを含み、アプリデータを公開するデータレイヤ。
ドメインレイヤというレイヤを追加することで、UI レイヤとデータレイヤの間のやり取りを簡素化でき、再利用できます。
最新のアプリ アーキテクチャ
この最新のアプリ アーキテクチャでは、特に次の手法の使用が推奨されています。
- リアクティブで階層的なアーキテクチャ。
- アプリのすべてのレイヤにおける単方向データフロー(UDF)。
- UI の複雑さを管理する状態ホルダーを含む UI レイヤ。
- コルーチンとフロー。
- 依存関係挿入のベスト プラクティス。
詳細については、以下の各セクションおよび目次にあるその他のアーキテクチャ ページと、最も重要なベスト プラクティスの概要が記載されている推奨事項ページをご覧ください。
UI レイヤ
UI レイヤ(またはプレゼンテーション レイヤ)の役割は、アプリデータを画面に表示することです。ユーザー操作(ボタンの押下など)または外部入力(ネットワーク レスポンスなど)によってデータが変更されるたびに、変更を反映するように UI を更新する必要があります。
UI レイヤは次の 2 つのもので構成されています。
- データを画面にレンダリングする UI 要素。これらの要素は、View または Jetpack Compose 関数を使用して作成します。
- データを保持して UI に公開し、ロジックを処理する状態ホルダー(ViewModel クラスなど)。
このレイヤについて詳しくは、UI レイヤのページをご覧ください。
データレイヤ
アプリのデータレイヤには、ビジネス ロジックが含まれています。ビジネス ロジックはアプリに価値をもたらすものであり、アプリがデータを作成、保存、変更する方法を決定するルールで構成されています。
データレイヤは、それぞれが 0 から多数のデータソースを含むことができるリポジトリで構成されています。アプリで処理するデータの種類ごとにリポジトリ クラスを作成する必要があります。たとえば、映画に関するデータであれば MoviesRepository
クラス、支払いに関するデータであれば PaymentsRepository
クラスを作成します。
リポジトリ クラスは、次のタスクを行います。
- アプリの他の部分にデータを公開する。
- データの変更を一元管理する。
- 複数のデータソース間の競合を解決する。
- アプリの他の部分からデータソースを抽象化する。
- ビジネス ロジックを格納する。
各データソース クラスは、ファイル、ネットワーク ソース、ローカル データベースなど、1 つのデータソースのみを処理する役割を担う必要があります。データソース クラスは、データ オペレーションのためにアプリとシステムの橋渡しをします。
このレイヤについて詳しくは、データレイヤのページをご覧ください。
ドメインレイヤ
ドメインレイヤは、UI レイヤとデータレイヤの間に位置するオプションのレイヤです。
ドメインレイヤは、複雑なビジネス ロジック、または複数の ViewModel で再利用される単純なビジネス ロジックをカプセル化します。すべてのアプリにこのような要件があるわけではないため、このレイヤはオプションです。複雑さに対処する場合や再利用性を優先する場合など、必要な場合にのみ使用してください。
通常、このレイヤのクラスを「ユースケース」または「インタラクタ」と呼びます。各ユースケースは 1 つの機能を担うべきです。たとえば、複数の ViewModel がタイムゾーンに基づいて適切なメッセージを画面に表示する場合、アプリに GetTimeZoneUseCase
クラスを持たせることができます。
このレイヤについて詳しくは、ドメインレイヤのページをご覧ください。
コンポーネント間の依存関係を管理する
アプリのクラスは、適切に機能するために他のクラスに依存しています。次のいずれかのデザイン パターンを使用して、特定のクラスの依存関係を収集できます。
- 依存関係の注入(DI): 依存関係の注入を利用すると、クラスの依存関係を構築することなく定義できます。ランタイムには、別のクラスがこの依存関係を提供します。
- サービス ロケータ: サービス ロケータ パターンでは、クラスが依存関係を作成せずに取得できるレジストリが提供されます。
こうしたパターンでは、コードが重複して煩雑になることなく依存関係を明確に管理できるため、コードの拡張が可能になります。さらに、テスト版と製品版の実装を簡単に切り替えることができます。
依存関係の注入のパターンに沿って、Android アプリで Hilt ライブラリを使用することをおすすめします。Hilt では、依存関係ツリーをたどって自動的にオブジェクトが構築され、コンパイル時の依存関係が保証され、Android フレームワーク クラスの依存関係コンテナが作成されます。
一般的なベスト プラクティス
プログラミングは創造的な活動であり、Android アプリの作成も例外ではありません。問題の解決方法は数多くあります。複数のアクティビティやフラグメント間でデータをやり取りする、リモートデータを取得してオフライン モード用にローカルで永続化するなど、重要なアプリで対処する一般的なシナリオにはさまざまなものがあります。
以下の推奨事項は必須ではありませんが、ほとんどの場合これに沿うことで、コードベースの堅牢性を高め、テストとメンテナンスを長期にわたって容易に実施できるようになります。
アプリのコンポーネントにデータを格納しないでください。
アプリのエントリ ポイント(アクティビティ、サービス、ブロードキャスト レシーバなど)をデータソースとして指定しないでください。その代わり、そのエントリ ポイントに関連するデータのサブセットを取得する他のコンポーネントとの調整のみを行う必要があります。ユーザーによるデバイスの操作や、システムの現在の全体的な稼働状態によっては、各アプリ コンポーネントの生存期間がかなり短くなります。
Android クラスへの依存を減らします。
アプリ コンポーネントは、Context
や Toast
など、Android フレームワーク SDK API に依存する唯一のクラスにする必要があります。そこからアプリの他のクラスを抽象化すると、テストがしやすくなり、アプリ内の結合を軽減できます。
アプリの各種モジュール間の役割の境界を明確に定義します。
たとえば、ネットワークからデータを読み込むコードを、コードベース内の複数のクラスやパッケージに散在させないでください。同様に、関連のない複数の処理(データ キャッシングとデータ バインディングなど)を同じクラスで定義しないでください。アプリの推奨アーキテクチャに沿うと便利です。
各モジュールからの公開はできるだけ行わないでください。
たとえば、モジュールの内部実装の詳細を公開するショートカットを作成しようとしないでください。短期的には時間を少し節約できるかもしれませんが、コードベースが発展するにつれて何倍もの技術的負債を負うことになる可能性があります。
アプリの特別な部分に焦点を当てて、他のアプリとの差別化を図ります。
同じボイラープレート コードを何度も書いてすでにあるものを作り直すのではなく、アプリを特別なものにすることに時間とエネルギーを集中させましょう。繰り返しのボイラープレート コードの記述には Jetpack ライブラリやその他の推奨ライブラリを利用してください。
アプリの各部分を個別にテストできるようにする方法を検討します。
たとえば、ネットワークからデータを取得するための明確に定義された API を用意することで、そのデータをローカル データベースに永続化するモジュールを簡単にテストできるようになります。そうしないで、2 つのモジュールのロジックを 1 か所に混在させたり、ネットワーク用のコードをコードベース全体に分散させたりすると、テストが不可能にはならないにしても、非常に困難になります。
型は同時実行ポリシーに関する責任を負います。
ある型が時間のかかるブロック処理を実行している場合、その型は適切なスレッドに計算を移動する責任を負います。その特定の型は、実行している計算のタイプと、その計算をどのスレッドで実行する必要があるかを認識します。型はメインセーフである(つまり、ブロックせずにメインスレッドから安全に呼び出せる)ことが必要です。
データの関連性と新鮮さをできる限り維持します。
こうすることで、デバイスがオフライン モードのときでも、ユーザーがアプリの機能を利用できるようになります。すべてのユーザーが常に高速な接続を利用できるわけではなく、たとえ利用できるとしても、混雑した場所では受信不良が起きる可能性があることに留意してください。
アーキテクチャのメリット
優れたアーキテクチャをアプリに実装することは、プロジェクト チームとエンジニアリング チームに次のような多くのメリットをもたらします。
- アプリ全体の保守性、品質、堅牢性が向上します。
- アプリのスケーリングが可能になります。より多くの人々とチームが、コードの競合を最小限に抑えながら、同じコードベースで開発に寄与できます。
- オンボーディングに役立ちます。アーキテクチャによってプロジェクトに一貫性がもたらされるため、新しいメンバーが速やかにチームに適応し、短時間でより効率的に作業できるようになります。
- テストが簡単になります。優れたアーキテクチャでは、一般的にテストしやすいシンプルな型が推奨されます。
- 適切に定義されたプロセスを使用して、体系的にバグを調査できます。
アーキテクチャへの投資は、ユーザーにも直接的な影響を及ぼします。エンジニアリング チームの生産性が高まることで、アプリの安定性と機能性が向上します。ただし、アーキテクチャの実装には事前の準備時間の投資も必要です。社内の各部門に対して実装を正当化するには、こちらのケーススタディが役立ちます。優れたアーキテクチャをアプリに実装した企業の成功事例が紹介されています。
サンプル
以下の Google サンプルは、優れたアプリ アーキテクチャを実証するものです。このガイダンスを実践するためにご利用ください。