Slide 1

Slide 1 text

Navigation in a Multiplatform World Choosing the Right Framework for your App Droidcon NYC - September '24 ! Ash Davies - SumUp Android & Kotlin GDE Berlin ashdavies.dev

Slide 2

Slide 2 text

blog.google/products/chrome/help-me-out-how-to-organize-chrome-tabs

Slide 3

Slide 3 text

! ashdavies.dev

Slide 4

Slide 4 text

val history = ArrayDeque() history.addLast(ForwardScreen) history.removeLast() ashdavies.dev

Slide 5

Slide 5 text

It's Complicated ashdavies.dev

Slide 6

Slide 6 text

® ashdavies.dev

Slide 7

Slide 7 text

m3.material.io/components/navigation-bar/overview

Slide 8

Slide 8 text

satukyrolainen.com/affordances

Slide 9

Slide 9 text

ashdavies.dev

Slide 10

Slide 10 text

android-developers.googleblog.com/2022/05/form-factors-google-io-22.html

Slide 11

Slide 11 text

learn.microsoft.com/en-us/windows/apps/design/basics/navigation-history-and-backwards-navigation

Slide 12

Slide 12 text

↑ ← ashdavies.dev

Slide 13

Slide 13 text

⃕ ! ashdavies.dev

Slide 14

Slide 14 text

medium.com/androiddevelopers/the-deep-links-crash-course-part2-deep-links-from-zero-to-hero-37f94cc8fb88

Slide 15

Slide 15 text

ashdavies.dev

Slide 16

Slide 16 text

ashdavies.dev/talks/navigation-and-the-single-activity-berlin

Slide 17

Slide 17 text

"Once we have gotten in to this entry-point to your UI, we really don't care how you organise the flow inside." — Dianne Hackborn, Android Framework team, 2016 ashdavies.dev

Slide 18

Slide 18 text

Jetpack Navigation ashdavies.dev

Slide 19

Slide 19 text

Fragments ashdavies.dev

Slide 20

Slide 20 text

2019 en.wikipedia.org/wiki/2019

Slide 21

Slide 21 text

2014: mortar & flow github.com/square/mortar developer.squareup.com/blog/simpler-android-apps-with-flow-and-mortar

Slide 22

Slide 22 text

ashdavies.dev

Slide 23

Slide 23 text

square/workflow square.github.io/workflow ashdavies.dev

Slide 24

Slide 24 text

square.github.io/workflow/historical/

Slide 25

Slide 25 text

2016: uber/ribs github.com/uber/RIBs uber.com/en-GB/blog/new-rider-app-architecture

Slide 26

Slide 26 text

Moving On... ashdavies.dev

Slide 27

Slide 27 text

Compose UI github.com/androidx/ androidx/tree/androidx- main/compose/ui ashdavies.dev

Slide 28

Slide 28 text

Compose UI • Declarative UI Framework • Open Source Kotlin • Accelerate UI development • Intuitive Idiomatic API ashdavies.dev

Slide 29

Slide 29 text

Obligatory Notice Compose != Compose UI ashdavies.dev

Slide 30

Slide 30 text

Compose is, at its core, a general- purpose tool for managing a tree of nodes of any type ... a “tree of nodes” describes just about anything, and as a result Compose can target just about anything. — Jake Wharton jakewharton.com/a-jetpack-compose-by-any-other-name

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

Jetpack Navigation Compose v2.4.0 (2021) • Build a navigation graph with a @Composable Kotlin DSL • Compose viewModel() scoped to navigation destination • Desintation level scope for rememberSaveable() • Automatic back handling support ashdavies.dev

Slide 33

Slide 33 text

Jetpack Navigation Compose < v2.8.0 private const val HOME_ROUTE = "home" NavHost( navController = navController, startDestination = HOME_ROUTE, ) { composable(route = HOME_ROUTE) { HomeScreen( onBackClick = navController::popBackStack, /* ... */ ) } } ashdavies.dev

Slide 34

Slide 34 text

Jetpack Navigation Compose < v2.8.0 private const val DETAIL_ID_KEY = "detailId" private const val DETAIL_ROUTE = "detail" NavHost( navController = navController, startDestination = DETAIL_ROUTE, ) { composable( route = DETAIL_ROUTE, arguments = listOf( navArgument(DETAIL_ID_KEY) { type = NavType.StringType defaultValue = null nullable = true } ) ) { DetailScreen(/* ... */) } } ashdavies.dev

Slide 35

Slide 35 text

Jetpack Navigation Compose < v2.8.0 private const val DETAIL_ID_KEY = "detailId" fun NavController.navigateToDetail(detailId: String) { navigate("detail?$DETAIL_ID_KEY=$detailId") } savedStateHandle.getStateFlow(DETAIL_ID_KEY, null) ashdavies.dev

Slide 36

Slide 36 text

Jetpack Navigation Compose v2.8.0 (04.09.2024) @Serializable data class DetailRoute(val id: String) NavHost( navController = navController, startDestination = "detail", ) { composable { DetailScreen(/* ... */) } } val route = savedStateHandle.toRoute() ashdavies.dev

Slide 37

Slide 37 text

medium.com/androiddevelopers/type-safe-navigation-for-compose-105325a97657

Slide 38

Slide 38 text

Navigation in a Multiplatform World Choosing the Right Framework for your App Android Navigation für N00bs by Some Dude ashdavies.dev

Slide 39

Slide 39 text

Kotlin Multiplatform Stable (1.9.20) ashdavies.dev

Slide 40

Slide 40 text

The Before-Times ashdavies.dev

Slide 41

Slide 41 text

The Before Times ashdavies.dev

Slide 42

Slide 42 text

ashdavies.dev

Slide 43

Slide 43 text

blog.jetbrains.com/kotlin/2023/04/kotlinconf-2023-opening-keynote/

Slide 44

Slide 44 text

ashdavies.dev

Slide 45

Slide 45 text

Maven Group ID Latest Update Stable Release Alpha Release annotation 04.09.2024 1.8.2 1.9.0-alpha03 collection 04.09.2024 1.4.3 1.5.0-alpha01 datastore 01.05.2024 1.1.1 - lifecycle 04.09.2024 2.8.5 2.9.0-alpha02 paging 07.08.2024 3.3.2 - room 21.08.2024 2.6.1 2.7.0-alpha07 sqlite 21.08.2024 2.4.0 2.5.0-alpha07 developer.android.com/kotlin/multiplatform | As of 15.09.2024

Slide 46

Slide 46 text

kotlin { sourceSets.commonMain.dependencies { implementation( "androidx.lifecycle:" + "lifecycle-viewmodel-ktx:" + "2.8.5" ) } } // Backed by ViewModelImpl public expect abstract class ViewModel cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ ViewModel.kt

Slide 47

Slide 47 text

Multiplatform Architecture ashdavies.dev

Slide 48

Slide 48 text

Decompose & Essenty arkivanov.github.io/Decompose ashdavies.dev

Slide 49

Slide 49 text

import com.arkivanov.decompose.ComponentContext class DefaultRootComponent( componentContext: ComponentContext, ) : RootComponent, ComponentContext by componentContext { init { lifecycle... // Access the Lifecycle stateKeeper... // Access the StateKeeper instanceKeeper... // Access the InstanceKeeper backHandler... // Access the BackHandler } } ashdavies.dev

Slide 50

Slide 50 text

class RootComponent(context: ComponentContext) : Root, ComponentContext { private val navigation = StackNavigation() override val childStack = childStack(/* ... */) fun createChild(config: Config, context: ComponentContext): Child = when (config) { is Config.List -> Child.List(ItemListComponent(context) { navigation.push(Config.Details(itemId = it)) } ) is Config.Details -> /* ... */ } } private sealed class Config : Parcelable { @Parcelize object List : Config() @Parcelize data class Details(val itemId: Long) : Config() } ashdavies.dev

Slide 51

Slide 51 text

Decompose • com.arkivanov.decompose:extensions-compose • com.arkivanov.decompose:extensions-android • com.arkivanov.essenty:state-keeper ashdavies.dev

Slide 52

Slide 52 text

Compose Multiplatform ashdavies.dev

Slide 53

Slide 53 text

Compose Multiplatform v1.0 | 2021 ashdavies.dev

Slide 54

Slide 54 text

Platform Stability level Android Stable iOS Stable Desktop (JVM) Stable Server-side (JVM) Stable Web based on Kotlin/Wasm Alpha Web based on Kotlin/JS Stable watchOS Best effort tvOS Best effort ashdavies.dev

Slide 55

Slide 55 text

Compose Multiplatform Jetpack Compose 1.6.11 1.6.7 1.6.10 1.6.7 1.6.2 1.6.4 ... ashdavies.dev

Slide 56

Slide 56 text

github.com/JetBrains/compose-multiplatform-core

Slide 57

Slide 57 text

kotlin { sourceSets.commonMain.dependencies { implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10") } } @Serializable data object HomeRoute NavHost(navController, HomeRoute) { composable { HomeScreen() } } val route = savedStateHandle.toRoute() ashdavies.dev

Slide 58

Slide 58 text

jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html

Slide 59

Slide 59 text

Compose Multiplatform Migration • Change artifact coordinates • Do nothing • Profit ashdavies.dev

Slide 60

Slide 60 text

The early bird gets the worm ... but the second mouse gets the cheese ashdavies.dev

Slide 61

Slide 61 text

Reactive Architecture • Push (not pull) • Unidirectional Data Flow • Declarative • Idempotent ashdavies.dev

Slide 62

Slide 62 text

Architecture Callbacks downloadManager.downloadFile("https://.../") { result -> fileManager.saveFile("storage/file", result) { success -> if (success) println("Downloaded file successfully") } } ashdavies.dev

Slide 63

Slide 63 text

Architecture Observables downloadManager.downloadFile("https://.../") .flatMap { result -> fileManager.saveFile("storage/file", result) } .observe { success -> if (success) println("Downloaded file successfully") } ashdavies.dev

Slide 64

Slide 64 text

Architecture Coroutines val file = downloadFile("https://.../") val success = fileManager.saveFile("storage/file", file) if (success) println("Downloaded file successfully") ashdavies.dev

Slide 65

Slide 65 text

Architecture Coroutines (Again) downloadManager.downloadFile("https://.../") .flatMapLatest { state -> when (state) { is State.Loaded -> stateFileManager.saveFile("storage/file", state.value) else -> state } } .collect { state -> when (state) { is State.Loading -> /* ... */ is State.Saved -> println("Downloaded file successfully") } } ashdavies.dev

Slide 66

Slide 66 text

Architecture Compose ashdavies.dev

Slide 67

Slide 67 text

Architecture Compose val downloadState = downloadManager .downloadFile("https://.../") .collectAsState(State.Loading) val fileState = when(downloadState) { is State.Loaded -> stateFileManager.saveFile("storage/file", state.value) else -> state } when (fileState) { is State.Loading -> /* ... */ is State.Saved -> LaunchedEffect(fileState) { println("Downloaded file successfully") } } ashdavies.dev

Slide 68

Slide 68 text

cashapp/molecule ashdavies.dev

Slide 69

Slide 69 text

Molecule fun CoroutineScope.launchCounter(): StateFlow { return launchMolecule(mode = ContextClock) { var count by remember { mutableStateOf(0) } LaunchedEffect(Unit) { while (true) { delay(1_000) count++ } } count } } ashdavies.dev

Slide 70

Slide 70 text

Demystifying Molecule Droidcon NYC 2022 ashdavies.dev/talks/demystifying-molecule-nyc

Slide 71

Slide 71 text

Role of Architecture ashdavies.dev

Slide 72

Slide 72 text

Pre-Compose Era ashdavies.dev

Slide 73

Slide 73 text

slackhq/circuit github.com/slackhq/circuit ashdavies.dev

Slide 74

Slide 74 text

Circuit • Supports most supported KMP platforms • Compose first architecture • Presenter & UI separation • Unidirectional Data Flow ashdavies.dev

Slide 75

Slide 75 text

Circuit State @Parcelize data object HomeScreen : Screen { data class State( val title: String, ): CircuitUiState } ashdavies.dev

Slide 76

Slide 76 text

Circuit Presenter class HomePresenter : Presenter { @Composable override fun present(): HomeScreen.State { return HomeScreen.State("Hello World") } } ashdavies.dev

Slide 77

Slide 77 text

Circuit UI @Composable fun HomeScreen( state: HomeScreen.State, modifier: Modifier = Modifier, ) { Text( text = state.title, modifier = modifier, ) } ashdavies.dev

Slide 78

Slide 78 text

Circuit val circuit = Circuit.Builder() .addPresenter(HomePresenter()) .addUi { _, _ -> HomeScreen(state, modifier) } .build() CircuitCompositionLocals(circuit) { val backStack = rememberSaveableBackStack(HomeScreen) NavigableCircuitContent( navigator = rememberCircuitNavigator(backStack), backStack = backStack, ) } ashdavies.dev

Slide 79

Slide 79 text

Circuit Navigation @Parcelize data object HomeScreen : Screen { data class State( val title: String, val eventSink: (Event) -> Unit ): CircuitUiState sealed interface Event { data class DetailClicked( val id: String, ): Event } ashdavies.dev

Slide 80

Slide 80 text

Circuit Navigation class HomePresenter(private val navigator: Navigator) : Presenter { @Composable override fun present(): HomeScreen.State { return HomeScreen.State("Hello World") { event -> when (event) { is HomeScreen.Event.DetailClicked -> navigator.goTo(DetailScreen(event.id)) } } } } ashdavies.dev

Slide 81

Slide 81 text

youtube.com/watch?v=ZIr_uuN8FEw

Slide 82

Slide 82 text

CircuitX • com.slack.circuit:circuitx-android • com.slack.circuit:circuitx-effects • com.slack.circuit:circuitx-gesture-navigation • com.slack.circuit:circuitx-overlays ashdavies.dev

Slide 83

Slide 83 text

rememberRetained() chrisbanes.me/posts/retaining-beyond-viewmodels

Slide 84

Slide 84 text

Circuit Examples • Chris Banes: Tivi github.com/chrisbanes/tivi • Zac Sweers: CatchUp github.com/ZacSweers/CatchUp • Zac Sweers: FieldSpottr github.com/zacsweers/fieldspottr • Ash Davies: Playground github.com/ashdavies/playground.ashdavies.dev ashdavies.dev

Slide 85

Slide 85 text

Full Disclosure Bias ashdavies.dev

Slide 86

Slide 86 text

adrielcafe/voyager voyager.adriel.cafe ashdavies.dev

Slide 87

Slide 87 text

Voyager class PostListScreen : Screen { @Composable override fun Content() { // ... } @Composable private fun PostCard(post: Post) { val navigator = LocalNavigator.currentOrThrow Card( modifier = Modifier.clickable { navigator.push(PostDetailsScreen(post.id)) } ) { // ... } } } ashdavies.dev

Slide 88

Slide 88 text

Voyager interface ParcelableScreen : Screen, Parcelable // Compile @Parcelize data class Post(/*...*/) : Parcelable @Parcelize data class ValidScreen( val post: Post ) : ParcelableScreen { // ... } // Not compile data class Post(/*...*/) @Parcelize data class ValidScreen( val post: Post ) : ParcelableScreen { // ... } ashdavies.dev

Slide 89

Slide 89 text

appyx bumble-tech.github.io/appyx ashdavies.dev

Slide 90

Slide 90 text

PreCompose github.com/Tlaster/PreCompose ashdavies.dev

Slide 91

Slide 91 text

Comparison androidx circuit decompose voyager workflow Multiplatform ✅ ✅ ✅ ✅ ✅ Compose 1st ❌ ✅ ❌ ✅ ❌ Documented* ❌ ✅ ✅ ✅ ❌ Ease-of-Use** ❌ ✅ / ✅ ❌ Opinionated*** ❌ ✅ ❌ ❌ ✅ * Documentation exists, but is outdated or hard to find ** Subjective, how quick to get started *** Additional API surface ashdavies.dev

Slide 92

Slide 92 text

Y Tho? ashdavies.dev

Slide 93

Slide 93 text

Good Code == Removable Code Code your own obscolescance ashdavies.dev

Slide 94

Slide 94 text

Thank You! Ash Davies - SumUp Android / Kotlin GDE Berlin ashdavies.dev

Slide 95

Slide 95 text

Don't Forget to Vote! ashdavies.dev