Android アーキテクチャに関する推奨事項

このページでは、アーキテクチャのベスト プラクティスと推奨事項をいくつか紹介します。これらを採用することで、アプリの品質、堅牢性、スケーラビリティを向上できます。また、アプリのメンテナンスとテストも容易になります。

以下のベスト プラクティスは、トピック別にグループ化されています。それぞれに、チームがどの程度それを推奨するかを示す優先度があります。優先事項のリストは次のとおりです。

  • 強く推奨: 自分のアプローチと根本的に矛盾しない限り、このプラクティスを実装する必要があります。
  • 推奨: このプラクティスで、アプリを改善できる可能性があります。
  • 省略可: このプラクティスで、特定の状況下でアプリを改善できる可能性があります。

階層型アーキテクチャ

Google が推奨する階層型アーキテクチャでは、関心の分離を重視しています。データモデルから UI を動作させ、信頼できる唯一の情報源の原則を遵守し、単方向データフローの原則に従います。階層型アーキテクチャに関するベスト プラクティスは次のとおりです。

推奨事項 説明
明確に定義されたデータレイヤを使用します データレイヤは、アプリデータをアプリの他の部分に公開し、アプリのビジネス ロジックの大部分を含みます。
  • リポジトリは、データソースが 1 つだけの場合でも作成する必要があります。
  • 小規模なアプリでは、data パッケージやモジュール内にデータレイヤ タイプを配置できます。
明確に定義された UI レイヤを使用します。 UI レイヤは、アプリケーション データを画面に表示するもので、ユーザー インタラクションの主要なポイントとして機能します。
  • 小規模なアプリでは、ui パッケージやモジュール内にデータレイヤ タイプを配置できます。
詳しくは、こちらの UI レイヤに関するベスト プラクティスをご覧ください
データレイヤでは、リポジトリを使用してアプリケーション データを公開する必要があります。

コンポーザブル、アクティビティ、ViewModel などの UI レイヤのコンポーネントを、データソースと直接やり取りさせないようにします。データソースの例:

  • データベース、DataStore、SharedPreferences、Firebase API
  • GPS 位置情報プロバイダ。
  • Bluetooth データ プロバイダ。
  • ネットワーク接続ステータス プロバイダ。
コルーチンと Flow を使用します。 コルーチンとフローを使用してレイヤ間の通信を行います。

その他のコルーチンに関するベスト プラクティスをご覧ください。

ドメインレイヤを使用します。 複数の ViewModel 間でデータレイヤとやり取りするビジネス ロジックを再利用する必要がある場合、または特定の ViewModel のビジネス ロジックの複雑さを簡素化したい場合に、ドメインレイヤを使用します。

UI レイヤ

UI レイヤの役割は、アプリデータを画面に表示することであり、ユーザー インタラクションの主要なポイントとして機能することです。UI レイヤのベスト プラクティスは次のとおりです。

推奨事項 説明
単方向データフロー(UDF)に従います。 単方向データフロー(UDF)の原則に従い、ViewModel はオブザーバー パターンを使用して UI の状態を公開し、メソッド呼び出しを介して UI からアクションを受け取ります。
メリットをアプリに適用できる場合は、AAC ViewModel を使用します。 AAC ViewModel を使用してビジネス ロジックを処理し、アプリデータを取得して UI の状態を UI(Compose または Android ビュー)に公開します。

詳しくは、ViewModel のベスト プラクティスをご覧ください。

ViewModel のメリットについては、こちらをご覧ください。

ライフサイクル対応 UI 状態コレクションを使用します。 適切なライフサイクル対応コルーチン ビルダー(UI では repeatOnLifecycle、Jetpack Compose では collectAsStateWithLifecycle)を使用して、UI から UI の状態を収集します。

詳しくは、repeatOnLifecycle をご覧ください。

詳しくは、collectAsStateWithLifecycle をご覧ください。

ViewModel から UI にイベントを送信しないようにします。 ViewModel でイベントをすぐに処理し、イベント処理の結果で状態を更新します。UI イベントについて詳しくは、こちらをご覧ください。
単一アクティビティのアプリケーションを使用します。 アプリに複数の画面がある場合、Navigation フラグメントまたは Navigation Compose を使用して画面間を移動し、アプリへのディープリンクを設定します。
Jetpack Compose を使用します。 Jetpack Compose を使用して、スマートフォン、タブレット、折りたたみ式デバイス、Wear OS 向けの新しいアプリを作成します。

次のスニペットは、ライフサイクル対応の方法で UI の状態を収集する方法を示しています。

View

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

Compose

@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
}

ViewModel

ViewModel は、UI の状態とデータレイヤへのアクセスを提供する役割を担います。ViewModel に関するベスト プラクティスは次のとおりです。

推奨事項 説明
ViewModel が、Android のライフサイクルに依存しないようにします。 ViewModel が、ライフサイクルに関連する型への参照を保持しないようにします。Activity, Fragment, Context または Resources を依存関係として渡さないようにします。ViewModel で Context を必要とする場合は、それが適切なレイヤにあるかどうかを強く評価する必要があります。
コルーチンと Flow を使用します。

ViewModel は、以下を使用してデータレイヤまたはドメインレイヤとやり取りします。

  • アプリケーション データを受信するための Kotlin Flow。
  • viewModelScope を使用してアクションを実行するための suspend 関数。
画面レベルで ViewModel を使用します。

再利用可能な UI で ViewModel を使用しないようにします。ViewModel は、以下で使用します。

  • 画面レベルのコンポーザブル。
  • View のアクティビティ / フラグメント。
  • Jetpack Navigation を使用する場合のデスティネーションまたはグラフ。
再利用可能な UI コンポーネントでは、プレーンな状態ホルダークラスを使用します。 再利用可能な UI コンポーネントの複雑さに対処するには、プレーンな状態ホルダークラスを使用します。これにより、状態を外部でホイスティングして制御できるようになります。
AndroidViewModel を使用しないようにします。 AndroidViewModel ではなく ViewModel クラスを使用します。Application クラスは ViewModel で使用しないようにします。代わりに、依存関係を UI またはデータレイヤに移行します。
UI 状態を公開します。 ViewModel が、uiState という単一のプロパティを介して UI にデータを公開する必要があります。UI に互いに関係のない複数のデータが表示されている場合、VM が複数の UI 状態プロパティを公開する可能性があります。
  • uiStateStateFlow にする必要があります。
  • データが階層の他のレイヤからのデータ ストリームとして来る場合は、stateIn 演算子と WhileSubscribed(5000) ポリシー(例)を使用して uiState を作成する必要があります。
  • データレイヤから来るデータ ストリームがない単純なケースでは、不変の StateFlow として公開される MutableStateFlow を使用できます(例)
  • ${Screen}UiState をデータクラスとして指定できます。データクラスには、データ、エラー、読み込みシグナルを含めることができます。異なる状態が排他的である場合、このクラスがシールクラスになることもあります。

次のスニペットは、ViewModel から UI の状態を公開する方法を示しています。

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

ライフサイクル

Android のライフサイクルを扱う際のベスト プラクティスは次のとおりです。

推奨事項 説明
アクティビティやフラグメントのライフサイクル メソッドをオーバーライドしないようにします。 アクティビティやフラグメントの onResume などのライフサイクル メソッドをオーバーライドしないようにします。代わりに LifecycleObserver を使用します。ライフサイクルが特定の Lifecycle.State に達したときにアプリが処理を実行する必要がある場合は、repeatOnLifecycle API を使用します。

次のスニペットは、特定のライフサイクル状態でオペレーションを行う方法を説明したものです。

View

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

Compose

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

依存関係を処理する

コンポーネント間の依存関係を管理する際は、いくつかのベスト プラクティスを遵守する必要があります。

推奨事項 説明
依存関係挿入を使用します。 依存関係挿入のベスト プラクティス、可能であれば特にコンストラクタ挿入のベスト プラクティスを活用してください。
必要に応じてコンポーネントにスコープを設定します。 型が共有する必要のある可変データを含む場合、あるいは、型がアプリ内で広く使用されており、その初期化にコストがかかる場合は、依存関係コンテナにスコープを設定します。
Hilt を使用します。 単純なアプリでは、Hilt または手動依存関係挿入を使用します。複雑なプロジェクトの場合は Hilt を使用します。たとえば、次の場合:
  • ViewModel を使用する複数の画面 - 統合
  • WorkManager の使用 - 統合
  • ナビゲーション グラフにスコープ設定された ViewModel など、Navigation の高度な使用 - 統合

テスト

テストを行う際のベスト プラクティスは次のとおりです。

推奨事項 説明
テストする内容を把握する

プロジェクトが Hello World アプリのように単純なものでない限り、最低でも以下でテストする必要があります。

  • フローを含む ViewModel の単体テスト。
  • データレイヤ エンティティの単体テスト。つまり、リポジトリとデータソースです。
  • CI の回帰テストとして役立つ UI ナビゲーション テスト。
モックよりもフェイクを優先します。 詳しくは、Android ドキュメントでテストダブルを使用するをご覧ください。
StateFlow をテストします。 StateFlow をテストする場合:

詳しくは、Android DAC でのテスト項目に関するガイドをご覧ください。

モデル

アプリでモデルを開発する場合は、以下のベスト プラクティスを実践する必要があります。

推奨事項 説明
複雑なアプリではレイヤごとにモデルを作成します。

複雑なアプリでは、必要に応じて、別のレイヤやコンポーネントで新しいモデルを作成します。以下の例を考えてみましょう。

  • リモート データソースは、ネットワーク経由で受け取るモデルを、アプリが必要とするデータのみを含むシンプルなクラスにマッピングできます
  • リポジトリは、UI レイヤが必要とする情報だけで DAO モデルをシンプルなデータクラスにマッピングできます。
  • ViewModel では、UiState クラスにデータレイヤ モデルを含めることができます。

命名規則

コードベースに名前を付けるときは、次のベスト プラクティスに注意する必要があります。

推奨事項 説明
メソッドの命名。
省略可
メソッドは動詞句である必要があります。たとえば、makePayment() です。
プロパティの命名。
省略可
プロパティは名詞句にする必要があります。たとえば、inProgressTopicSelection です。
データのストリームの命名。
省略可
クラスが Flow ストリーム、LiveData、またはその他のストリームを公開する場合、命名規則は get{model}Stream() です。たとえば、getAuthorStream(): Flow<Author> で関数がモデルのリストを返す場合、モデル名は複数形(getAuthorsStream(): Flow<List<Author>>)にする必要があります。
インターフェース実装の命名。
省略可
インターフェース実装の名前はわかりやすいものにする必要があります。適切な名前が見つからない場合は、接頭辞として Default を使用します。たとえば、NewsRepository インターフェースの場合には、OfflineFirstNewsRepository または InMemoryNewsRepository を使用できます。適切な名前がまったく見つからない場合は、DefaultNewsRepository を使用します。FakeAuthorsRepository のように、フェイクの実装には Fake の接頭辞を付ける必要があります。