Deferred Navigation(遅延ナビゲーション)とは何か、原則と実装

著者: IT Sectr 公開日: 2026-06-10 読了時間: 9 分

Deferred Navigation(遅延ナビゲーション)は、非同期操作の完了後に次の画面への遷移が行われるパターンであり、ユーザーの操作の瞬間に直接行われるわけではありません。Android Developers (2024) によると、遅延ナビゲーションはナビゲーションとデータ読み込みの間の競合状態を回避し、プッシュ通知やディープリンクからの遷移処理を簡素化します。主な違いは、必要なデータがすべて利用可能になった後にルートが計算されることです。

重要なポイント

  • Deferred Navigation — 非同期操作の完了後に遷移が実行される遅延ナビゲーション
  • 直接ナビゲーションは遷移を即座に処理し、遅延ナビゲーションはデータを待つ
  • 典型的なシナリオ: 認証、ディープリンク、プッシュ通知、設定読み込み
  • Androidでは Navigation Component とコールバック、StateFlow で実装
  • iOSでは Combine または async/await とナビゲーションコーディネーターを使用

Deferred Navigation とは

Deferred Navigation(遅延ナビゲーション)は、必要なデータがすべて利用可能になるまでナビゲーションの決定を延期するアーキテクチャパターンです。ユーザーがボタンを押してすぐに新しい画面に移動する直接遷移とは異なり、遅延ナビゲーションはトリガーイベントと実際の遷移を分離し、その間に非同期操作を配置します。

アーキテクチャ的には、Deferred Navigation は状態変更に基づいています。ボタンを押すと非同期プロセスが開始され、その結果へのサブスクリプションがナビゲーションをトリガーします。これは MVVM や MVI アーキテクチャのアプリケーションで特に重要であり、ViewModel が状態を管理し、View(Activity、Fragment、SwiftUI View)が変更をサブスクライブして遷移で反応します。このアプローチにより、UI とナビゲーションロジックの間の直接的な依存関係が排除されます。

Google I/O 2023 によると、遅延ナビゲーションは、ネットワークリクエスト、認証チェック、設定の読み込み、権限の結果にナビゲーションが依存するすべてのシナリオで推奨されています。このパターンはディープリンクを処理する際にも必須であり、アプリはまず起動し、ルート画面を読み込み、その後にターゲットルートにナビゲートする必要があります。

遅延ナビゲーションが必要な場合

Deferred Navigationは、直接ナビゲーションが誤った画面状態や読み込みエラーにつながるシナリオで使用されます。遅延ナビゲーションが必要な4つの主要なケースを見てみましょう。

認証と認可

ユーザーが保護されたコンテンツをタップした場合、アプリはまずアクセストークンを確認する必要があります。トークンが期限切れの場合、直接ナビゲーションでコンテンツ画面に移動すると、空の画面や401エラーが発生します。遅延ナビゲーションはトークンを確認し、成功した場合のみターゲット画面にナビゲートします。失敗した場合はログイン画面にリダイレクトします。

ディープリンクの処理

外部リンクからアプリが開かれた場合、まずルート画面を読み込み、ナビゲーション状態を復元し、その後にディープリンク遷移を実行する必要があります。ルートコンテキストなしでターゲット画面に直接ナビゲートすると、空のナビゲーションスタックや壊れたバックスタックなどの異常が発生します。

コンテンツ付きプッシュ通知

プッシュ通知をタップしたとき、アプリは閉じている、バックグラウンド、アクティブの3つの状態のいずれかになります。Deferred Navigation はアプリの状態を判断し、必要なコンテンツを読み込み、その後にターゲット画面を表示します。iOS では UNNotificationContentExtension を使用してこのシナリオを処理できます。

動的フィーチャーフラグ

画面の機能がサーバーからのフィーチャーフラグで制御されている場合、遅延ナビゲーションはまず設定をリクエストし、その後に画面を表示することを可能にします。機能が無効になっている場合、ユーザーは空の画面の代わりに代替コンテンツまたはプレースホルダーを表示します。

シナリオ直接ナビゲーションDeferred Navigation
認証期限切れトークンで空画面ログインにリダイレクト
ディープリンク壊れたバックスタック正しいナビゲーションスタック
プッシュコンテキストなしで読み込み遷移前にデータ準備完了
フィーチャーフラグ利用不可機能の表示プレースホルダーまたは代替

Deferred Navigation vs 直接ナビゲーション

直接ナビゲーションは、イベントに応答して即座に遷移が実行される従来のアプローチです。ユーザーがボタンを押すと、UIルーターがすぐに画面を切り替えます。このアプローチはシンプルで予測可能ですが、サーバーからのデータや条件チェックが必要なシナリオでは制限があります。

Deferred Navigationは、非同期状態というレイヤーを追加します。ユーザーイベントが操作を開始し、その結果へのサブスクリプションがナビゲーションを制御します。これによりコードの複雑さは増しますが、柔軟性が得られます。同じトリガーでも、読み込まれたデータに応じて異なる画面に遷移できます。

2つのアプローチの選択は要件によって異なります。画面の表示に非同期データが必要ない場合は直接ナビゲーションを使用します。画面がリクエストの結果、認証、または外部条件に依存する場合は遅延ナビゲーションが必要です。一部の遷移を直接、一部を遅延とするハイブリッドアプローチが、業務用アプリケーションで最も一般的な手法です。

Android での実装: Navigation Component と ViewModel

Android Jetpack は、アーキテクチャレベルで Deferred Navigation を実装するメカニズムを提供します。基本的な考え方は、ViewModel が状態を管理し、Activity または Fragment が変更をサブスクライブして NavController を介してナビゲーションをトリガーすることです。

StateFlow を使用した Deferred Navigation

StateFlow は Kotlin コルーチンにおいて、遅延ナビゲーションに最適なツールです。ViewModel がナビゲーションイベントで StateFlow を更新し、Activity がそれを監視して遷移を実行します。イベントが処理されると StateFlow がクリアされ、繰り返しのナビゲーションを防ぎます。

kotlin
class MainViewModel : ViewModel() {
    private val _navigation = MutableSharedFlow<NavigationEvent>()
    val navigation: SharedFlow<NavigationEvent> = _navigation

    fun onDeepLinkReceived(link: String) {
        viewModelScope.launch {
            val data = resolveDeepLink(link)
            _navigation.emit(NavigationEvent.GoToScreen(data))
        }
    }
}

NavController を使用した Deferred Navigation

Activity では、ナビゲーションへのサブスクリプションが ViewModel からのルートで NavController をトリガーします。画面回転時の繰り返しナビゲーションを防ぐために、イベントを1回だけ処理する NavigationEventWrapper ラッパーが使用されます。Jetpack Navigation 2.7+ はタイプセーフな引数渡しのために Safe Args をサポートしています。

kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val vm: MainViewModel by viewModels()
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            vm.navigation.collect { event ->
                when (event) {
                    is NavigationEvent.GoToScreen ->
                        findNavController(R.id.nav_host)
                            .navigate(event.route)
                }
            }
        }
    }
}

iOS での実装: Coordinator Pattern と Combine

iOS には Android Jetpack のような組み込みの Navigation Component がないため、開発者は Combine または async/await と組み合わせた Coordinator Pattern を通じて Deferred Navigation を実装します。Coordinator は画面スタックを管理し、読み込まれたデータに基づいてナビゲーションの決定を行います。

Combine を使用した Coordinator

Coordinator は、ViewController 間のナビゲーションを管理するオブジェクトです。Combine と組み合わせて、ViewModel が PassthroughSubject を介してイベントを公開し、Coordinator がそれらをサブスクライブして遷移を実行します。このアプローチは UI をナビゲーションロジックから完全に分離し、Apple のアプリケーションアーキテクチャに関する推奨事項に準拠しています。

swift
final class AppCoordinator {
    private var cancellables = Set<AnyCancellable>()

    func start(viewModel: MainViewModel) {
        viewModel.$navigationDestination
            .compactMap { $0 }
            .sink { [weak self] destination in
                self?.navigateTo(destination)
            }
            .store(in: &cancellables)
    }
}

async/await を使用した Deferred Navigation

Swift 5.5 は構造化された並行性を導入し、Combine なしで async/await を使用した遅延ナビゲーションの実装を可能にしました。ViewModel はデータ読み込み後にルートを返す async 関数を提供します。Coordinator は Task 内でこの関数を呼び出し、受け取ったルートに基づいて遷移を実行します。

swift
class AuthViewModel: ObservableObject {
    func resolveDeeplink(_ url: URL) async -> AppRoute? {
        guard let token = await AuthService.shared.getValidToken() else { return .login }
        return await DeeplinkRouter.resolve(url, token: token)
    }
}

// In Coordinator:
Task {
    if let route = await viewModel.resolveDeeplink(url) {
        navigateTo(route)
    }
}

よくある間違いとベストプラクティス

Deferred Navigation は非同期シナリオの処理を簡素化しますが、状態管理には規律あるアプローチが必要です。開発者が遅延ナビゲーションを実装する際に犯す主な間違いを見てみましょう。

間違い: 初期化完了前のナビゲーション

最も一般的な間違いは、ルート画面が完全に初期化され、NavController または Coordinator が遷移の準備ができる前に Deferred Navigation を実行しようとすることです。Android では IllegalStateException が発生し、iOS では未定義の UI 状態になります。解決策は、ナビゲーションをトリガーする前にコンポーネントのライフサイクルが STARTED または RESUMED 状態であることを確認することです。

間違い: 画面に戻る際の繰り返しナビゲーション

StateFlow または Subject が処理後にイベントをクリアしない場合、前の画面に戻るとユーザーが自動的に同じ画面にリダイレクトされる可能性があります。Android では replay=0 の SharedFlow を、iOS では処理後に CurrentValueSubject を nil で使用し、ナビゲーションイベントが1回だけ発生するようにします。

ベストプラクティス: 単一の NavController または Coordinator

アプリケーション内のすべてのナビゲーションに 1つの中央 コンポーネントを使用します。各 Activity、Fragment、または ViewController が独自のナビゲーションコントローラーを持つと、アプリケーションの異なる部分間での遅延ナビゲーションが混沌とします。単一の Coordinator により、ナビゲーションシナリオのデバッグとテストが容易になります。

ベストプラクティス: 遅延ナビゲーションシナリオのテスト

Deferred Navigation は直接ナビゲーションよりもテストが困難です。非同期操作により時間要素が導入されるためです。Android では TestDispatcher(kotlinx-coroutines-test)を、iOS では XCTestExpectation を使用してデータ読み込みをシミュレートし、ナビゲーションが期待されるルートに従うことを確認します。各シナリオの分離テストのために、認証サービスとディープリンクサービスをモック化します。

よくある質問

Deferred Navigation と Deep Link の違いは何ですか?

Deferred Navigation は、あらゆる非同期シナリオで適用できる遅延遷移パターンです。Deep Link は遅延ナビゲーションのトリガーの1つですが、唯一のものではありません。認証やフィーチャーフラグも遅延ナビゲーションを使用します。

遅延ナビゲーションと直接ナビゲーションを組み合わせられますか?

はい、ほとんどのアプリケーションはハイブリッドアプローチを使用しています。非同期依存関係のない製品一覧画面は直接ナビゲーションを、データ読み込みのある詳細画面は遅延ナビゲーションを使用できます。区分けは各画面のアーキテクチャによって決まります。

遅延ナビゲーションで競合状態を回避するには?

使用してください Android では replay なしの SharedFlow、iOS ではバッファリングなしの combineLatest を。新しいトリガーで以前のサブスクリプションをキャンセルします。これにより、最新のナビゲーションイベントのみが処理されることが保証されます。

Jetpack Compose は遅延ナビゲーションをサポートしていますか?

はい、Compose では ViewModel の StateFlow をサブスクライブし、LaunchedEffect で NavController.navigate を呼び出すことで遅延ナビゲーションを実装します。Google は遅延シナリオにはイベント駆動モデルの Navigation Compose を推奨しています。

アプリが最小化されている場合の遅延ナビゲーションの処理方法は?

遅延させてください アプリが再びアクティブになるまでナビゲーションを。Android では Lifecycle.State.STARTED を使用してイベントをフィルタリングします。iOS では Combine または async/await ブロックで UIApplication.State を確認します。

まとめ

  • Deferred Navigation — 非同期操作の完了後に遷移が発生するパターン
  • 主なシナリオ: 認証、ディープリンク、プッシュ通知、フィーチャーフラグ
  • StateFlow/SharedFlow(Android)と Combine/async-await(iOS)が実装ツール
  • Deferred Navigation は直接ナビゲーションと異なり、非同期読み込み時の競合状態を引き起こさない
  • Navigation Component(Android)と Coordinator Pattern(iOS)が基本アーキテクチャ
  • 単一の NavController/Coordinator が一貫したナビゲーションに必須
  • テスト には TestDispatcher と非同期サービスのモックが必要

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください