Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
TimeTreeのNavigation 3移行記
Search
Dai Miyamoto
October 08, 2025
280
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
TimeTreeのNavigation 3移行記
DroidKaigi 2025 後夜祭
Dai Miyamoto
October 08, 2025
More Decks by Dai Miyamoto
See All by Dai Miyamoto
スムーズな価格改定を実現するためのベストプラクティス
dai1678
0
310
Camera Xライブラリの魅力と最新機能を紐解く
dai1678
0
2.5k
Featured
See All Featured
Visualization
eitanlees
152
17k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.6k
RailsConf 2023
tenderlove
30
1.5k
Ethics towards AI in product and experience design
skipperchong
2
360
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.2k
Git: the NoSQL Database
bkeepers
PRO
432
67k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Fireside Chat
paigeccino
42
4k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
11k
SEO for Brand Visibility & Recognition
aleyda
0
4.7k
So, you think you're a good person
axbom
PRO
2
2.1k
Transcript
宮本 ⼤ DroidKaigi 2025 後夜祭 TimeTreeのNavigation 3移⾏記
発表者紹介 宮本 ⼤ Miyamoto Dai • 2025年2⽉ 株式会社TimeTree 中途⼊社 •
公開カレンダーチーム Androidエンジニア • 社内ニックネーム: Danny • DroidKaigi 2022ではCamera Xについて登壇 @dairoid7774 Dai1678
None
発表の概要 • DroidKaigi 2025 セッション 「Navigation 2 を 3 に移⾏する(予定)ためにやったこと」
のTimeTree版 • 違いは対象アーキテクチャ [DroidKaigi セッション] Navigation Compose ↓ Navigation 3 (移⾏予定) [今回の発表] Multi Activity ↓ Navigation 3 (導⼊中)
この発表で得られること Navigation 3の完全理解 TimeTreeでの導⼊事例と知⾒ • Multi Activityでの段階的移⾏アプローチ • 導⼊中に遭遇した課題と対処法
Navigation 3 おさらい • Jetpack Composeに最適化されたナビゲーションライブラリ • 2025年5⽉ Google I/Oで発表
• オープンで拡張性の⾼い設計 従来のナビゲーション バックスタックを フレームワーク / ライブラリが管理 Navigation 3 バックスタックを 開発者が管理‧制御
バックスタック管理‧制御 // 画⾯を表すKeyを定義 data object ProductList data class ProductDetail(val id:
String) @Composable fun MyApp() { // バックスタックはListで管理 val backStack = remember { mutableStateListOf<Any>(ProductList) } // バックスタックにKeyを追加すると、ライブラリが状態を更新してくれる backStack.add(ProductDetail(id = "ABC")) // バックスタックからKeyを削除すると画⾯を破棄できる backStack.removeLastOrNull() }
Navigation 3 導⼊のきっかけ • AndroidXライブラリのAlphaは機能的に安定 • APIのは快適変更リスクを承知の上で導⼊決定 • 新しい施策のタイミングで新技術を試すチャンス ⼤きな新機能追加等を控えているため
内容は開発中のため秘密🤫 なぜAlphaバージョンを採⽤?
NavDisplay と Single Activity • バックスタックを監視してコンテンツをレンダリング • 全ての画⾯のコンテナとして機能 • Single
Activity構成が必要 NavDisplayとは Activity A Activity B Activity C Activity NavDisplay - 〇〇Screen - ✕✕Screen
モジュール構成 features-xxx - XXXScreen components-navigation - NavEntryProviderのinterface定義 - NavKeyの定義 app
NavEntryProviderの実装クラス features-root NavDisplayを持つ新しい ルーティングActivity NavEntryProvider.kt interface NavEntryProvider<T : NavKey> : () -> (T) -> NavEntry<T> interface RootNavEntryProvider : NavEntryProvider<RootNavKey> RootNavKey.kt sealed class RootNavKey : NavKey { @Serializable data object Calendar : RootNavKey() @Serializable data object Settings : RootNavKey() }
モジュール構成 features-calendar - CalendarScreen components-navigation NavEntryProviderのinterface定義 app NavEntryProviderの実装クラス features-root NavDisplayを持つ新しい
ルーティングActivity NavEntryProviderImpl.kt class RootNavEntryProviderImpl @Inject constructor() : RootNavEntryProvider { override fun invoke(): (RootNavKey) -> NavEntry<RootNavKey> = entryProvider { entry<RootNavKey.Calendar> { CalendarScreen() } entry<RootNavKey.Settings> { SettingsScreen() } } }
モジュール構成 features-xxx - XXXScreen components-navigation NavEntryProviderのinterface定義 app NavEntryProviderの実装クラス features-root NavDisplayを持つ新しい
ルーティングActivity RootNavDisplay.kt @Composable fun RootNavDisplay( rootNavEntryProvider: RootNavEntryProvider, ) { val rootBackStack = rememberNavBackStack(RootNavKey.Calendar) CompositionLocalProvider( LocalRootBackStack provides rootBackStack, ) { NavDisplay( backStack = rootBackStack.backStack, entryProvider = rootNavEntryProvider(), entryDecorators = listOf( rememberSceneSetupNavEntryDecorator(), rememberSavedStateNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator(), ), ) } }
段階的移⾏ app NavEntryProviderの実装クラス features-root NavDisplayを持つ新しい ルーティングActivity features-legacy-home これまでログイン後に遷移していた ホーム画⾯を持つActivity features-app-navigation
- Feature Flag値によって新旧どちらのActivityを起動 するか判断し、遷移先ActivityのIntentを返す AppStarter.kt @Singleton class AppStarter @Inject constructor( private val context: Context, ) { val defaultIntent: Intent get() = if (/** リニューアル後の場合 */) { Intent(context, RootActivity::class.java) } else { Intent(context, LegacyHomeActivity::class.java) } val restoreToFrontIntent: Intent get() = defaultIntent.apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } }
Navigation 3導⼊中で⾏った⼯夫 主な課題 ① ディープリンク‧PUSH通知からの起動 バックスタックの構築が必要 • 従来: TaskStackBuilder •
Nav3: ⾃前でListを管理 ③ Activitiyベースの遷移との共存 Composable内にActivityに依存する処理があると移⾏しづらい • Activityに依存する処理をComposableから削除する
⼯夫① ディープリンク‧PUSH通知からの起動 Activity起動後にバックスタックの構築を明⽰的に⾏う AppStarter.kt fun openCalendarEvent(): Intent = Intent(context, RootActivity::class.java).apply
{ putExtra(“build_back_stack”, LaunchedAction.CalendarEvent) }
⼯夫① ディープリンク‧PUSH通知からの起動 Activity起動後にバックスタックの構築を明⽰的に⾏う RememberOnNewIntent.kt @Composable fun rememberNewIntent(): Intent? { val
activity = LocalActivity.current as ComponentActivity? ?: return null var intent by remember { mutableStateOf<Intent?>(null) } DisposableEffect(activity) { val onNewIntentListener = Consumer { newIntent: Intent -> intent = newIntent } activity.addOnNewIntentListener(onNewIntentListener) onDispose { activity.removeOnNewIntentListener(onNewIntentListener) } } return intent }
⼯夫① ディープリンク‧PUSH通知からの起動 Activity起動後にバックスタックの構築を明⽰的に⾏う BuildBackStackHandler.kt @Composable fun BuildBackStackHandler() { val launchedAction:
LaunchedAction? = rememberNewIntent()?.getParcelableExtra(“build_back_stack”, LaunchedAction::class.java) val rootBackStack = LocalRootBackStack.current LaunchedEffect(launchedAction) { when (launchedAction) { is LaunchedAction.CalendarEvent -> { rootBackStack.add(RootNavKey.CalendarEvent) } } } }
⼯夫② Activitiyベースの遷移との共存 Composable内でActivityに依存している CalendarEventScreen.kt @Composable fun CalendarEventScreen() { Scaffold( topBar
= { TopAppBar( title = { … } navigationIcon = { IconButton(onClick = { activity?.finish() }) { … } ) } ) { … } }
⼯夫② Activitiyベースの遷移との共存 Activityに依存する処理はホスティングする CalendarEventScreen.kt @Composable fun CalendarEventScreen( onBack: () ->
Unit, ) { Scaffold( topBar = { TopAppBar( title = { … } navigationIcon = { IconButton(onClick = onBack) { … } ) } ) { … } } CalendarEventActivity.kt @AndroidEntryPoint class CalendarEventActivity : ComponentActivity { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { TimeTreeTheme { CalendarEventScreen(onBack = { finish() }) } } } }
⼯夫② Activitiyベースの遷移との共存 Navigation 3に依存する処理もホスティングする NavEntryProviderImpl.kt class RootNavEntryProviderImpl @Inject constructor() :
RootNavEntryProvider { override fun invoke(): (RootNavKey) -> NavEntry<RootNavKey> = entryProvider { entry<RootNavKey.CalendarEvent> { val backstack = LocalRootBackStack.current CalendarEventScreen( onBack = { backstack.removeLastOrNull() } ) } } }
⼯夫② Activitiyベースの遷移との共存 Activityでフィールドインジェクションしたものを使⽤ CalendarEventScreen.kt @Composable fun CalendarEventScreen( eventLogger: EventLogger, )
{ … } CalendarEventActivity.kt @AndroidEntryPoint class CalendarEventActivity : ComponentActivity { @field:Inject internal latent var eventLogger: EventLogger override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { TimeTreeTheme { CalendarEventScreen(eventLogger = eventLogger) } } } }
⼯夫② Activitiyベースの遷移との共存 Composable内でインジェクトする CalendarEventScreen.kt @Composable fun CalendarEventScreen() { val activity
= LocalActivity.current val entryPoint = EntryPointAccessors.fromActivity( activity = requreNotActivity(activity) entryPoint = CalendarEventScreenEntryPoint::class.java, ) entryPoint.eventLogger().logEvent() } CalendarEventScreenEntryPoint.kt @EntryPoint @InstallIn(ActivityComponent::class) interface CalendarEventScreenEntryPoint { fun eventLogger(): EventLogger }
今後の展望 現在: 部分的なNavigation 3 導⼊ NavDisplay Activity + 既存Activity郡のハイブリッド構成 中期⽬標:
全画⾯のCompose化 既存画⾯を段階的にCompose化してNavDisplay管理下に移⾏ 🚧道のりは⻑い... Composeに移⾏できていない画⾯が多数 最終⽬標: 完全なNavigation 3構成 全画⾯がNavDisplayを持つ⼀つのActivityの元で管理される ✨2ペインレイアウトなど⼤画⾯デバイス最適化の実現
Appendix • Android Developers ナビゲーション 3 https://developer.android.com/guide/navigation/navigat ion-3?hl=ja • Navigation
2 を 3 に移⾏する(予定) ためにやったこと https://speakerdeck.com/yokomii/navigation-2-wo-3-niyi -xing-suru-yu-ding-tameniyatutakoto • nav3-recipes https://github.com/android/nav3-recipes エンジニア採⽤中! →採⽤ページはコチラ 懇親会でもぜひお話しましょう!
None