Deferred Navigation(遅延ナビゲーション)は、非同期操作の完了後に次の画面への遷移が行われるパターンであり、ユーザーの操作の瞬間に直接行われるわけではありません。Android Developers (2024) によると、遅延ナビゲーションはナビゲーションとデータ読み込みの間の競合状態を回避し、プッシュ通知やディープリンクからの遷移処理を簡素化します。主な違いは、必要なデータがすべて利用可能になった後にルートが計算されることです。
重要なポイント
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 |
|---|---|---|
| 認証 | 期限切れトークンで空画面 | ログインにリダイレクト |
| ディープリンク | 壊れたバックスタック | 正しいナビゲーションスタック |
| プッシュ | コンテキストなしで読み込み | 遷移前にデータ準備完了 |
| フィーチャーフラグ | 利用不可機能の表示 | プレースホルダーまたは代替 |
直接ナビゲーションは、イベントに応答して即座に遷移が実行される従来のアプローチです。ユーザーがボタンを押すと、UIルーターがすぐに画面を切り替えます。このアプローチはシンプルで予測可能ですが、サーバーからのデータや条件チェックが必要なシナリオでは制限があります。
Deferred Navigationは、非同期状態というレイヤーを追加します。ユーザーイベントが操作を開始し、その結果へのサブスクリプションがナビゲーションを制御します。これによりコードの複雑さは増しますが、柔軟性が得られます。同じトリガーでも、読み込まれたデータに応じて異なる画面に遷移できます。
2つのアプローチの選択は要件によって異なります。画面の表示に非同期データが必要ない場合は直接ナビゲーションを使用します。画面がリクエストの結果、認証、または外部条件に依存する場合は遅延ナビゲーションが必要です。一部の遷移を直接、一部を遅延とするハイブリッドアプローチが、業務用アプリケーションで最も一般的な手法です。
Android Jetpack は、アーキテクチャレベルで Deferred Navigation を実装するメカニズムを提供します。基本的な考え方は、ViewModel が状態を管理し、Activity または Fragment が変更をサブスクライブして NavController を介してナビゲーションをトリガーすることです。
StateFlow は Kotlin コルーチンにおいて、遅延ナビゲーションに最適なツールです。ViewModel がナビゲーションイベントで StateFlow を更新し、Activity がそれを監視して遷移を実行します。イベントが処理されると StateFlow がクリアされ、繰り返しのナビゲーションを防ぎます。
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))
}
}
}
Activity では、ナビゲーションへのサブスクリプションが ViewModel からのルートで NavController をトリガーします。画面回転時の繰り返しナビゲーションを防ぐために、イベントを1回だけ処理する NavigationEventWrapper ラッパーが使用されます。Jetpack Navigation 2.7+ はタイプセーフな引数渡しのために Safe Args をサポートしています。
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 には Android Jetpack のような組み込みの Navigation Component がないため、開発者は Combine または async/await と組み合わせた Coordinator Pattern を通じて Deferred Navigation を実装します。Coordinator は画面スタックを管理し、読み込まれたデータに基づいてナビゲーションの決定を行います。
Coordinator は、ViewController 間のナビゲーションを管理するオブジェクトです。Combine と組み合わせて、ViewModel が PassthroughSubject を介してイベントを公開し、Coordinator がそれらをサブスクライブして遷移を実行します。このアプローチは UI をナビゲーションロジックから完全に分離し、Apple のアプリケーションアーキテクチャに関する推奨事項に準拠しています。
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)
}
}
Swift 5.5 は構造化された並行性を導入し、Combine なしで async/await を使用した遅延ナビゲーションの実装を可能にしました。ViewModel はデータ読み込み後にルートを返す async 関数を提供します。Coordinator は Task 内でこの関数を呼び出し、受け取ったルートに基づいて遷移を実行します。
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回だけ発生するようにします。
アプリケーション内のすべてのナビゲーションに 1つの中央 コンポーネントを使用します。各 Activity、Fragment、または ViewController が独自のナビゲーションコントローラーを持つと、アプリケーションの異なる部分間での遅延ナビゲーションが混沌とします。単一の Coordinator により、ナビゲーションシナリオのデバッグとテストが容易になります。
Deferred Navigation は直接ナビゲーションよりもテストが困難です。非同期操作により時間要素が導入されるためです。Android では TestDispatcher(kotlinx-coroutines-test)を、iOS では XCTestExpectation を使用してデータ読み込みをシミュレートし、ナビゲーションが期待されるルートに従うことを確認します。各シナリオの分離テストのために、認証サービスとディープリンクサービスをモック化します。
よくある質問
Deferred Navigation は、あらゆる非同期シナリオで適用できる遅延遷移パターンです。Deep Link は遅延ナビゲーションのトリガーの1つですが、唯一のものではありません。認証やフィーチャーフラグも遅延ナビゲーションを使用します。
はい、ほとんどのアプリケーションはハイブリッドアプローチを使用しています。非同期依存関係のない製品一覧画面は直接ナビゲーションを、データ読み込みのある詳細画面は遅延ナビゲーションを使用できます。区分けは各画面のアーキテクチャによって決まります。
使用してください Android では replay なしの SharedFlow、iOS ではバッファリングなしの combineLatest を。新しいトリガーで以前のサブスクリプションをキャンセルします。これにより、最新のナビゲーションイベントのみが処理されることが保証されます。
はい、Compose では ViewModel の StateFlow をサブスクライブし、LaunchedEffect で NavController.navigate を呼び出すことで遅延ナビゲーションを実装します。Google は遅延シナリオにはイベント駆動モデルの Navigation Compose を推奨しています。
遅延させてください アプリが再びアクティブになるまでナビゲーションを。Android では Lifecycle.State.STARTED を使用してイベントをフィルタリングします。iOS では Combine または async/await ブロックで UIApplication.State を確認します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。