Slide 1

Slide 1 text

Lessons learned decoupling Architecture Components from platform specific code Jeremy Woods, Marcello Galhardo linkedIn@jeremybwoods 🦋 @marcellogalhardo.dev

Slide 2

Slide 2 text

Architecture Components Team

Slide 3

Slide 3 text

● 6 people across 3 countries. Architecture Components Team

Slide 4

Slide 4 text

● 6 people across 3 countries. ● 12 different libraries ○ Activity, DataStore, Fragment, Hilt, Lifecycle, Navigation, Paging, Room, SavedState, ViewModel, SQLite, XProcessing Architecture Components Team ● 6 people across 3 countries.

Slide 5

Slide 5 text

● 6 people across 3 countries. ● 14 different libraries ○ Lifecycle, SavedState, ViewModel. Navigation, Activity, Fragment, Room, DataStore, Paging, Dagger Hilt, SQLite, XProcessing. ○ Navigation3, NavigationEvent Architecture Components Team ● 6 people across 3 countries. ● 1 different libraries ○ Activity, DataStore, Fragment, Hilt, Lifecycle, Navigation, Paging, Room, SavedState, ViewModel, SQLite, XProcessing

Slide 6

Slide 6 text

Today’s agenda 01 02 03 04

Slide 7

Slide 7 text

Today’s agenda Motivation: why KMP? 01 02 03 04

Slide 8

Slide 8 text

Today’s agenda Motivation: why KMP? 01 Migrating an existing library 02 03 04

Slide 9

Slide 9 text

Today’s agenda Motivation: why KMP? 01 Migrating an existing library 02 Creating a new library 03 04

Slide 10

Slide 10 text

Today’s agenda Motivation: why KMP? 01 Migrating an existing library 02 Creating a new library 03 04 Review

Slide 11

Slide 11 text

Proprietary + Confidential 01 Motivation: why KMP?

Slide 12

Slide 12 text

Why KMP?

Slide 13

Slide 13 text

● Android is Kotlin-first (since 2019). Why KMP?

Slide 14

Slide 14 text

● Android is Kotlin-first (since 2019). ● Code sharing across native platforms. Why KMP? ● Android is Kotlin-first (since 2019).

Slide 15

Slide 15 text

● Android is Kotlin-first (since 2019). ● Code sharing across native platforms. ● Help building the ecosystem. Why KMP? ● Android is Kotlin-first (since 2019). ● Code sharing across native platforms.

Slide 16

Slide 16 text

AndroidX Arch KMP Libraries

Slide 17

Slide 17 text

AndroidX Arch KMP Libraries

Slide 18

Slide 18 text

AndroidX Arch KMP Libraries

Slide 19

Slide 19 text

Proprietary + Confidential 02 Migrating an existing library

Slide 20

Slide 20 text

● Used to save and restore state in a process death. ● First alpha released in December 2018, written in Java. ● KMP-compatible release in May 2025 (1.3.0). AndroidX SavedState

Slide 21

Slide 21 text

Core Components

Slide 22

Slide 22 text

Core Components

Slide 23

Slide 23 text

How these APIs look?

Slide 24

Slide 24 text

public class SavedStateRegistryController { public val savedStateRegistry: SavedStateRegistry public fun performRestore(savedState: Bundle?) public fun performSave(outBundle: Bundle) // {...} }

Slide 25

Slide 25 text

public class SavedStateRegistryController { public val savedStateRegistry: SavedStateRegistry public fun performRestore(savedState: Bundle?) public fun performSave(outBundle: Bundle) // {...} }

Slide 26

Slide 26 text

public class SavedStateRegistryController { public val savedStateRegistry: SavedStateRegistry public fun performRestore(savedState: Bundle?) public fun performSave(outBundle: Bundle) // {...} }

Slide 27

Slide 27 text

public class SavedStateRegistry { public fun consumeRestoredStateForKey(key: String): Bundle? public fun registerSavedStateProvider(key: String, provider: SavedStateProvider) public fun interface SavedStateProvider { public fun saveState(): Bundle } // {...} }

Slide 28

Slide 28 text

public class SavedStateRegistry { public fun consumeRestoredStateForKey(key: String): Bundle? public fun registerSavedStateProvider(key: String, provider: SavedStateProvider) public fun interface SavedStateProvider { public fun saveState(): Bundle } // {...} }

Slide 29

Slide 29 text

public class SavedStateRegistry { public fun consumeRestoredStateForKey(key: String): Bundle? public fun registerSavedStateProvider(key: String, provider: SavedStateProvider) public fun interface SavedStateProvider { public fun saveState(): Bundle } // {...} }

Slide 30

Slide 30 text

class ComponentActivity : Activity() { val controller = SavedStateRegistryController.create(owner = this) override fun onCreate(savedInstanceState: Bundle?) { controller.performRestore(savedInstanceState) // {...} } override fun onSaveInstanceState(outState: Bundle) { // {...} controller.performSave(outState) } }

Slide 31

Slide 31 text

class ComponentActivity : Activity() { val controller = SavedStateRegistryController.create(owner = this) override fun onCreate(savedInstanceState: Bundle?) { controller.performRestore(savedInstanceState) // {...} } override fun onSaveInstanceState(outState: Bundle) { // {...} controller.performSave(outState) } }

Slide 32

Slide 32 text

class ComponentActivity : Activity() { val controller = SavedStateRegistryController.create(owner = this) override fun onCreate(savedInstanceState: Bundle?) { controller.performRestore(savedInstanceState) // {...} } override fun onSaveInstanceState(outState: Bundle) { // {...} controller.performSave(outState) } }

Slide 33

Slide 33 text

// Or a more high-level API. class MyViewModel( private val handle: SavedStateHandle, ) : ViewModel() { val name = handle.getMutableStateFlow(“key”) { “John Doe” } }

Slide 34

Slide 34 text

Why?

Slide 35

Slide 35 text

Top 50k apps using SavedState

Slide 36

Slide 36 text

class Bundle { constructor(capacity: Int) constructor(b: Bundle?) constructor(persistableBundle: PersistableBundle) fun putAll(bundle: Bundle) fun size(): Int fun isEmpty(): Boolean fun clear() public override fun clone(): Any fun putBoolean(key: String, value: Boolean) fun putByte(key: String, value: Byte) fun putChar(key: String, value: Char) fun putShort(key: String, value: Short) fun putInt(key: String, value: Int) fun putLong(key: String, value: Long) fun putFloat(key: String, value: Float) fun putDouble(key: String, value: Double) fun putString(key: String, value: String?) fun putCharSequence(key: String, value: CharSequence?) fun putParcelable(key: String, value: Parcelable?) fun putParcelableArray(key: String, value: Array?) fun putParcelableArrayList(key: String, value: ArrayList?) fun putSparseParcelableArray(key: String, value: android.util.SparseArray?) fun putSerializable(key: String, value: Serializable?) fun putIntegerArrayList(key: String, value: ArrayList?) fun putStringArrayList(key: String, value: ArrayList?) fun putCharSequenceArrayList(key: String, value: ArrayList?) fun putBundle(key: String, value: Bundle?) fun putBooleanArray(key: String, value: BooleanArray?) fun putByteArray(key: String, value: ByteArray?) fun putShortArray(key: String, value: ShortArray?) fun putCharArray(key: String, value: CharArray?) fun putIntArray(key: String, value: IntArray?) fun putLongArray(key: String, value: LongArray?) fun putFloatArray(key: String, value: FloatArray?) fun putDoubleArray(key: String, value: DoubleArray?) fun putStringArray(key: String, value: Array?) fun putCharSequenceArray(key: String, value: Array?) fun putParcelableArray(key: String, value: Array?) fun putSize(key: String, value: Size?) fun putSizeF(key: String, value: SizeF?) fun getBoolean(key: String): Boolean fun getBoolean(key: String, defaultValue: Boolean): Boolean fun getByte(key: String): Byte fun getByte(key: String, defaultValue: Byte): Byte fun getChar(key: String): Char fun getChar(key: String, defaultValue: Char): Char fun getShort(key: String): Short fun getShort(key: String, defaultValue: Short): Short fun getInt(key: String): Int fun getInt(key: String, defaultValue: Int): Int fun getLong(key: String): Long fun getLong(key: String, defaultValue: Long): Long fun getFloat(key: String): Float fun getFloat(key: String, defaultValue: Float): Float fun getDouble(key: String): Double fun getDouble(key: String, defaultValue: Double): Double fun getString(key: String): String? fun getCharSequence(key: String): CharSequence? fun getParcelable(key: String): T? fun getParcelableArray(key: String): Array? fun getParcelableArrayList(key: String): ArrayList? fun getSparseParcelableArray(key: String): android.util.SparseArray? fun getSerializable(key: String): Serializable? fun getIntegerArrayList(key: String): ArrayList? fun getStringArrayList(key: String): ArrayList? fun getCharSequenceArrayList(key: String): ArrayList? fun getBundle(key: String): Bundle? fun getBooleanArray(key: String): BooleanArray? fun getByteArray(key: String): ByteArray? fun getShortArray(key: String): ShortArray? fun getCharArray(key: String): CharArray? fun getIntArray(key: String): IntArray? fun getLongArray(key: String): LongArray? fun getFloatArray(key: String): FloatArray? fun getDoubleArray(key: String): DoubleArray? fun getStringArray(key: String): Array? fun getCharSequenceArray(key: String): Array? fun getSize(key: String): Size? fun getSizeF(key: String): SizeF? fun containsKey(key: String): Boolean fun remove(key: String) fun keySet(): Set }

Slide 37

Slide 37 text

public class SavedStateRegistryController { // {...} public fun performRestore(savedState: Bundle?) public fun performSave(outBundle: Bundle) // {...} }

Slide 38

Slide 38 text

public class SavedStateRegistryController { // {...} public fun performRestore(savedState: Bundle?) public fun performSave(outBundle: Bundle) // {...} }

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

Android Framework Team !== AndroidX Team

Slide 41

Slide 41 text

“Extension is shadowed by a member”

Slide 42

Slide 42 text

Zero-cost abstraction

Slide 43

Slide 43 text

public class SavedStateRegistryController { // {...} public fun performRestore(savedState: Bundle?) public fun performSave(outBundle: Bundle) // {...} }

Slide 44

Slide 44 text

public class SavedStateRegistryController { // {...} public fun performRestore(savedState: SavedState?) public fun performSave(outBundle: SavedState) // {...} }

Slide 45

Slide 45 text

Expect Actual

Slide 46

Slide 46 text

// commonMain public expect class SavedState // androidMain public actual typealias SavedState = android.os.Bundle // nonAndroidMain public actual class SavedState(map: MutableMap)

Slide 47

Slide 47 text

// commonMain public expect class SavedState // androidMain public actual typealias SavedState = android.os.Bundle // nonAndroidMain public actual class SavedState(map: MutableMap)

Slide 48

Slide 48 text

// commonMain public expect class SavedState // androidMain public actual typealias SavedState = android.os.Bundle // nonAndroidMain public actual class SavedState(map: MutableMap)

Slide 49

Slide 49 text

public expect inline fun savedState(block: SavedStateWriter.() -> Unit): SavedState @JvmInline public expect value class SavedStateReader(source: SavedState) // {...} public inline fun SavedState.read(block: SavedStateReader.() -> T): T // {...} @JvmInline public expect value class SavedStateWriter(source: SavedState) // {...} public inline fun SavedState.write(block: SavedStateWriter.() -> T): T // {...}

Slide 50

Slide 50 text

public expect inline fun savedState(block: SavedStateWriter.() -> Unit): SavedState @JvmInline public expect value class SavedStateReader(source: SavedState) // {...} public inline fun SavedState.read(block: SavedStateReader.() -> T): T // {...} @JvmInline public expect value class SavedStateWriter(source: SavedState) // {...} public inline fun SavedState.write(block: SavedStateWriter.() -> T): T // {...}

Slide 51

Slide 51 text

public expect inline fun savedState(block: SavedStateWriter.() -> Unit): SavedState @JvmInline public expect value class SavedStateReader(source: SavedState) // {...} public inline fun SavedState.read(block: SavedStateReader.() -> T): T // {...} @JvmInline public expect value class SavedStateWriter(source: SavedState) // {...} public inline fun SavedState.write(block: SavedStateWriter.() -> T): T // {...}

Slide 52

Slide 52 text

public expect inline fun savedState(block: SavedStateWriter.() -> Unit): SavedState @JvmInline public expect value class SavedStateReader(source: SavedState) // {...} public inline fun SavedState.read(block: SavedStateReader.() -> T): T // {...} @JvmInline public expect value class SavedStateWriter(source: SavedState) // {...} public inline fun SavedState.write(block: SavedStateWriter.() -> T): T // {...}

Slide 53

Slide 53 text

public expect inline fun savedState(block: SavedStateWriter.() -> Unit): SavedState @JvmInline public expect value class SavedStateReader(source: SavedState) // {...} public inline fun SavedState.read(block: SavedStateReader.() -> T): T // {...} @JvmInline public expect value class SavedStateWriter(source: SavedState) // {...} public inline fun SavedState.write(block: SavedStateWriter.() -> T): T // {...}

Slide 54

Slide 54 text

SavedState, Readers and Writers

Slide 55

Slide 55 text

data class Person(val name: String, val age: Int) val person = Person(“John Doe”, 21) val state = savedState { this: SavedStateWriter -> putString("name", person.name) putInt("age", person.age) } val name = state.read { this: SavedStateReader -> getString("name") }

Slide 56

Slide 56 text

data class Person(val name: String, val age: Int) val person = Person(“John Doe”, 21) val state = savedState { this: SavedStateWriter -> putString("name", person.name) putInt("age", person.age) } val name = state.read { this: SavedStateReader -> getString("name") }

Slide 57

Slide 57 text

data class Person(val name: String, val age: Int) val person = Person(“John Doe”, 21) val state = savedState { this: SavedStateWriter -> putString("name", person.name) putInt("age", person.age) } val name = state.read { this: SavedStateReader -> getString("name") }

Slide 58

Slide 58 text

KotlinX Serialization + Compose

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

data class Person(val name: String, val age: Int)

Slide 62

Slide 62 text

@Serializable data class Person(val name: String, val age: Int)

Slide 63

Slide 63 text

@Serializable data class Person(val name: String, val age: Int) fun main() { val person = Person("John Doe", 21) val encoded: SavedState = encodeToSavedState(person) val decoded: Person = decodeFromSavedState(encoded) }

Slide 64

Slide 64 text

@Serializable data class Person(val name: String, val age: Int) fun main() { val person = Person("John Doe", 21) val encoded: SavedState = encodeToSavedState(person) val decoded: Person = decodeFromSavedState(encoded) }

Slide 65

Slide 65 text

@Serializable data class Person(val name: String, val age: Int) fun main() { val person = Person("John Doe", 21) val encoded: SavedState = encodeToSavedState(person) val decoded: Person = decodeFromSavedState(encoded) }

Slide 66

Slide 66 text

@Serializable data class Person(val name: String, val age: Int) fun main() { val person = Person("John Doe", 21) val encoded: SavedState = encodeToSavedState(person) val decoded: Person = decodeFromSavedState(encoded) }

Slide 67

Slide 67 text

Bundle = [ "name": String = "John Doe", "age" : Int = 21, ]

Slide 68

Slide 68 text

@Serializable data class Person(val name: String, val age: Int) fun main() { val configuration = SavedStateConfiguration { serializersModule = SerializersModule {…} encodeDefaults = true classDiscriminatorMode = ClassDiscriminatorMode.POLYMORPHIC } val person = Person("John Doe", 21) val encoded: SavedState = encodeToSavedState(configuration, person) val decoded: Person = decodeFromSavedState(configuration encoded) }

Slide 69

Slide 69 text

@Serializable data class Person(val name: String, val age: Int) class MyViewModel(handle: SavedStateHandle) : ViewModel() { val person by handle.saved { Person("John Doe", 21) } }

Slide 70

Slide 70 text

@Serializable data class Person(val name: String, val age: Int) class MyComponent : SavedStateRegistryOwner { val person by saved { Person("John Doe", 21) } }

Slide 71

Slide 71 text

@Serializable data class Person(val name: String, val age: Int) class MyActivity : ComponentActivity() { val person by saved { Person("John Doe", 21) } }

Slide 72

Slide 72 text

@Serializable data class Person(val name: String, val age: Int) class MyFragment : Fragment() { val person by saved { Person("John Doe", 21) } }

Slide 73

Slide 73 text

@Serializable data class Person(val name: String, val age: Int) val navController = rememberNavController() NavHost(navController, startDestination = Profile) { composable { entry: NavBackStackEntry -> val selectedUser by remember { entry.saved { Person("John Doe", 21) } } Profile(navController) } }

Slide 74

Slide 74 text

@Serializable data class Person(val name: String, val age: Int) val backStack = rememberNavBackStack() NavDisplay(backStack, entryProvider = entryProvider { entry { val owner = LocalSavedStateRegistryOwner.current.savedStateRegistry val selectedUser by remember { owner.saved { Person("John Doe", 21) } } Profile(navController) } )

Slide 75

Slide 75 text

KotlinX Serialization + Compose

Slide 76

Slide 76 text

@Serializable data class Person(val name: String, val age: Int) @Composable fun MyComposable() { val saveable = rememberSaveable(serializer = serializer()) { Person("John Doe", 21) } }

Slide 77

Slide 77 text

@Serializable data class Person(val name: String, val age: Int) @Composable fun MyComposable() { val saveable = rememberSaveable(serializer = serializer()) { Person("John Doe", 21) } }

Slide 78

Slide 78 text

@Serializable data class Person(val name: String, val age: Int) val backStack = rememberNavBackStack() NavDisplay(backStack, entryProvider = entryProvider { entry { val owner = LocalSavedStateRegistryOwner.current val registry = owner.savedStateRegistry val person by remember { registry.saved { Person("John Doe", 21) } } Profile(person) } )

Slide 79

Slide 79 text

@Serializable data class Person(val name: String, val age: Int) val backStack = rememberNavBackStack() NavDisplay(backStack, entryProvider = entryProvider { entry { val person by rememberSaveable(serializer = serializer()) { Person("John Doe", 21) } Profile(person) } )

Slide 80

Slide 80 text

Proprietary + Confidential 03 Creating a new library for KMP

Slide 81

Slide 81 text

What is Navigation Event?

Slide 82

Slide 82 text

● System Back ○ Predictive Back What is Navigation Event?

Slide 83

Slide 83 text

● System Back ○ Predictive Back ● Keyboard Events What is Navigation Event? ● System Back ○ Predictive Back

Slide 84

Slide 84 text

● System Back ○ Predictive Back ● Keyboard Events ● Forward Navigation What is Navigation Event? ● System Back ○ Predictive Back ● Keyboard Events

Slide 85

Slide 85 text

OnBackPressedDispatcher

Slide 86

Slide 86 text

OnBackPressedDispatcher ● Added in the AndroidX Activity 1.0.0 release

Slide 87

Slide 87 text

OnBackPressedDispatcher ● Added in the AndroidX Activity 1.0.0 release ● Available from ComponentActivity ● Added in the AndroidX Activity 1.0.0 release

Slide 88

Slide 88 text

OnBackPressedDispatcher ● Added in the AndroidX Activity 1.0.0 release ● Available from ComponentActivity ● Register OnBackPressedCallback to listen for events ● Added in the AndroidX Activity 1.0.0 release ● Available from ComponentActivity

Slide 89

Slide 89 text

OnBackPressedDispatcher ● Added in the AndroidX Activity 1.0.0 release ● Available from ComponentActivity ● Register OnBackPressedCallback to listen for events ● Predictive/BackHandler in Compose ● Added in the AndroidX Activity 1.0.0 release ● Available from ComponentActivity ● Register OnBackPressedCallback to listen for events

Slide 90

Slide 90 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackStarted(backEvent: BackEventCompat) {...} override fun handleOnBackProgressed(backEvent: BackEventCompat) {...} override fun handleOnBackPressed() {...} override fun handleOnBackCancelled() {...} }) } } OnBackPressedDispatcher

Slide 91

Slide 91 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackStarted(backEvent: BackEventCompat) {...} override fun handleOnBackProgressed(backEvent: BackEventCompat) {...} override fun handleOnBackPressed() {...} override fun handleOnBackCancelled() {...} }) } } OnBackPressedDispatcher

Slide 92

Slide 92 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackStarted(backEvent: BackEventCompat) {...} override fun handleOnBackProgressed(backEvent: BackEventCompat) {...} override fun handleOnBackPressed() {...} override fun handleOnBackCancelled() {...} }) } } OnBackPressedDispatcher

Slide 93

Slide 93 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackStarted(backEvent: BackEventCompat) {...} override fun handleOnBackProgressed(backEvent: BackEventCompat) {...} override fun handleOnBackPressed() {...} override fun handleOnBackCancelled() {...} }) } } OnBackPressedDispatcher

Slide 94

Slide 94 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackStarted(backEvent: BackEventCompat) {...} override fun handleOnBackProgressed(backEvent: BackEventCompat) {...} override fun handleOnBackPressed() {...} override fun handleOnBackCancelled() {...} }) } } OnBackPressedDispatcher

Slide 95

Slide 95 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackStarted(backEvent: BackEventCompat) {...} override fun handleOnBackProgressed(backEvent: BackEventCompat) {...} override fun handleOnBackPressed() {...} override fun handleOnBackCancelled() {...} }) } } OnBackPressedDispatcher

Slide 96

Slide 96 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackStarted(backEvent: BackEventCompat) {...} override fun handleOnBackProgressed(backEvent: BackEventCompat) {...} override fun handleOnBackPressed() {...} override fun handleOnBackCancelled() {...} }) } } OnBackPressedDispatcher

Slide 97

Slide 97 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback { /* handle system back here */ } } } OnBackPressedDispatcher

Slide 98

Slide 98 text

class MyComponent(activity: ComponentActivity) { val dispatcher = activity.onBackPressedDispatcher init { dispatcher.addCallback { /* handle system back here */ } } } OnBackPressedDispatcher

Slide 99

Slide 99 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 100

Slide 100 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 101

Slide 101 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 102

Slide 102 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 103

Slide 103 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 104

Slide 104 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 105

Slide 105 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 106

Slide 106 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 107

Slide 107 text

@Composable fun myComposable() { PredictiveBackHandler(true /* enabled condition */) { backEvent -> /* handleOnBackStarted */ try { backEvent.collect { /* handleOnBackProgressed */} /* handleOnBackPressed */ } finally { /* handleOnBackCancelled */ } } } PredictiveBackHandler

Slide 108

Slide 108 text

Brief Vocabulary

Slide 109

Slide 109 text

System Back Trio onBackPressedCallback onBackInvokedCallback NavigationEventCallback OnBackPressedDispatcher OnBackInvokedDispatcher NavigationEventDispatcher Platform AndroidX AndroidX

Slide 110

Slide 110 text

OnBackPressedDispatcher

Slide 111

Slide 111 text

OnBackPressedDispatcher

Slide 112

Slide 112 text

OnBackPressedDispatcher

Slide 113

Slide 113 text

OnBackPressedDispatcher

Slide 114

Slide 114 text

OnBackPressedDispatcher

Slide 115

Slide 115 text

OnBackPressedDispatcher

Slide 116

Slide 116 text

OnBackPressedDispatcher

Slide 117

Slide 117 text

Back Up Forward Home Android Phone ✅ ✅ ❓ ✅ Android Tablet ✅ ✅ ❓ ✅ Android XR ✅ ✅ ❓ ✅ Web (Browser) ✅ ✅ ✅ ❓ iOS ✅ ❓ ❓ ✅ Desktop (Multiplatform) ✅ ❓ ❓ ❓ System Events on other Platforms

Slide 118

Slide 118 text

Navigation Event

Slide 119

Slide 119 text

Navigation Event

Slide 120

Slide 120 text

Navigation Event

Slide 121

Slide 121 text

Expect Actual

Slide 122

Slide 122 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 123

Slide 123 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 124

Slide 124 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 125

Slide 125 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 126

Slide 126 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 127

Slide 127 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 128

Slide 128 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 129

Slide 129 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 130

Slide 130 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect Classes

Slide 131

Slide 131 text

Navigation Event

Slide 132

Slide 132 text

Navigation Event

Slide 133

Slide 133 text

Verifying the new approach

Slide 134

Slide 134 text

● Re-write OnBackPressedDispatcher on top of NavigationEvent Verifying the new approach

Slide 135

Slide 135 text

● Re-write OnBackPressedDispatcher on top of NavigationEvent Verifying the new approach

Slide 136

Slide 136 text

● Re-write OnBackPressedDispatcher on top of NavigationEvent ● Allow integration with the new Navigation3 library Verifying the new approach ● Re-write OnBackPressedDispatcher on top of NavigationEvent

Slide 137

Slide 137 text

OnBackPressed Rewrite

Slide 138

Slide 138 text

OnBackPressed Rewrite

Slide 139

Slide 139 text

OnBackPressed Rewrite

Slide 140

Slide 140 text

OnBackPressed Rewrite

Slide 141

Slide 141 text

OnBackPressed Rewrite

Slide 142

Slide 142 text

OnBackPressed Rewrite

Slide 143

Slide 143 text

OnBackPressed Rewrite

Slide 144

Slide 144 text

OnBackPressed Rewrite

Slide 145

Slide 145 text

public expect class NavigationEvent { public constructor( touchX: Float, touchY: Float, progress: Float, swipeEdge: @SwipeEdge Int, frameTimeMillis: Long = 0 ) } Expect NavigationEvent

Slide 146

Slide 146 text

public actual class NavigationEvent actual constructor(...) { public constructor(backEvent: BackEvent) : this( touchX = backEvent.touchX, touchY = backEvent.touchY, progress = backEvent.progress, swipeEdge = backEvent.swipeEdge, frameTimeMillis = backEvent.frameTimeMillis, ) } Actual NavigationEvent

Slide 147

Slide 147 text

public actual class NavigationEvent actual constructor(...) { public constructor(backEvent: BackEvent) : this( touchX = backEvent.touchX, touchY = backEvent.touchY, progress = backEvent.progress, swipeEdge = backEvent.swipeEdge, frameTimeMillis = backEvent.frameTimeMillis, ) } Actual NavigationEvent

Slide 148

Slide 148 text

public expect class NavigationInputHandler { private val dispatcher: NavigationEventDispatcher internal fun updateActiveState() } Expect NavigationInputHandler

Slide 149

Slide 149 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 150

Slide 150 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 151

Slide 151 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 152

Slide 152 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 153

Slide 153 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 154

Slide 154 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 155

Slide 155 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 156

Slide 156 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 157

Slide 157 text

internal actual fun updateActiveState() { val shouldBeRegistered = dispatcher.hasEnabledCallbacks()\ if (shouldBeRegistered && !registered) { registerOnBackInvokedCallback(...) } else if (!shouldBeRegistered && registered) { unregisterOnBackInvokedCallback(...) } } Actual NavigationInputHandler

Slide 158

Slide 158 text

internal actual fun updateActiveState() { val shouldBeRegistered = dispatcher.hasEnabledCallbacks() if (shouldBeRegistered && !registered) { registerOnBackInvokedCallback(...) } else if (!shouldBeRegistered && registered) { unregisterOnBackInvokedCallback(...) } } Actual NavigationInputHandler

Slide 159

Slide 159 text

internal actual fun updateActiveState() { val shouldBeRegistered = dispatcher.hasEnabledCallbacks() if (shouldBeRegistered && !registered) { registerOnBackInvokedCallback(...) } else if (!shouldBeRegistered && registered) { unregisterOnBackInvokedCallback(...) } } Actual NavigationInputHandler

Slide 160

Slide 160 text

public actual class NavigationInputHandler(val dispatcher:...) { private var invoker: OnBackInvokedDispatcher? = null private var onBackInvokedCallback: OnBackInvokedCallback? = null private var registered = false public fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) { this.invoker = invoker updateActiveState() } init { ... } internal actual fun updateActiveState() { ... } } Actual NavigationInputHandler

Slide 161

Slide 161 text

public class NavigationInputHandler(val dispatcher:...) { private var onBackInvokedCallback: OnBackInvokedCallback? = null init { onBackInvokedCallback: OnBackInvokedCallback = OnBackAnimationCallback { override fun onBackStarted(backEvent: BackEvent) { dispatcher.dispatchOnStarted(NavigationEvent(backEvent)) } override fun onBackProgressed(backEvent: BackEvent) { dispatcher.dispatchOnProgressed(NavigationEvent(backEvent)) } override fun onBackInvoked() { dispatcher.dispatchOnCompleted() } override fun onBackCancelled() { dispatcher.dispatchOnCancelled() } } } } Actual NavigationInputHandler

Slide 162

Slide 162 text

public class NavigationInputHandler(val dispatcher:...) { private var onBackInvokedCallback: OnBackInvokedCallback? = null init { onBackInvokedCallback: OnBackInvokedCallback = OnBackAnimationCallback { override fun onBackStarted(backEvent: BackEvent) { dispatcher.dispatchOnStarted(NavigationEvent(backEvent)) } override fun onBackProgressed(backEvent: BackEvent) { dispatcher.dispatchOnProgressed(NavigationEvent(backEvent)) } override fun onBackInvoked() { dispatcher.dispatchOnCompleted() } override fun onBackCancelled() { dispatcher.dispatchOnCancelled() } } } } Actual NavigationInputHandler

Slide 163

Slide 163 text

public class NavigationInputHandler(val dispatcher:...) { private var onBackInvokedCallback: OnBackInvokedCallback? = null init { onBackInvokedCallback: OnBackInvokedCallback = OnBackAnimationCallback { override fun onBackStarted(backEvent: BackEvent) { dispatcher.dispatchOnStarted(NavigationEvent(backEvent)) } override fun onBackProgressed(backEvent: BackEvent) { dispatcher.dispatchOnProgressed(NavigationEvent(backEvent)) } override fun onBackInvoked() { dispatcher.dispatchOnCompleted() } override fun onBackCancelled() { dispatcher.dispatchOnCancelled() } } } } Actual NavigationInputHandler

Slide 164

Slide 164 text

abstract class OnBackPressedCallback(enabled: Boolean) { internal val eventCallback = object : NavigationEventCallback(isEnabled = enabled) { override fun onEventStarted(event: NavigationEvent) { handleOnBackStarted(BackEventCompat(event)) } override fun onEventProgressed(event: NavigationEvent) { handleOnBackProgressed(BackEventCompat(event)) } ... } } OnBackPressedCallback

Slide 165

Slide 165 text

abstract class OnBackPressedCallback(enabled: Boolean) { internal val eventCallback = object : NavigationEventCallback(isEnabled = enabled) { override fun onEventStarted(event: NavigationEvent) { handleOnBackStarted(BackEventCompat(event)) } override fun onEventProgressed(event: NavigationEvent) { handleOnBackProgressed(BackEventCompat(event)) } ... } } OnBackPressedCallback

Slide 166

Slide 166 text

class OnBackPressedDispatcher(...) { fun addCallback(onBackPressedCallback: OnBackPressedCallback) { eventDispatcher.addCallback(onBackPressedCallback.eventCallback) } private fun onBackStarted(backEvent: BackEventCompat) { eventDispatcher.dispatchOnStarted(backEvent.toNavigationEvent()) } private fun onBackProgressed(backEvent: BackEventCompat) { eventDispatcher.dispatchOnProgressed(backEvent.toNavigationEvent()) } ... } OnBackPressedDispatcher

Slide 167

Slide 167 text

class OnBackPressedDispatcher(...) { fun addCallback(onBackPressedCallback: OnBackPressedCallback) { eventDispatcher.addCallback(onBackPressedCallback.eventCallback) } private fun onBackStarted(backEvent: BackEventCompat) { eventDispatcher.dispatchOnStarted(backEvent.toNavigationEvent()) } private fun onBackProgressed(backEvent: BackEventCompat) { eventDispatcher.dispatchOnProgressed(backEvent.toNavigationEvent()) } ... } OnBackPressedDispatcher

Slide 168

Slide 168 text

class OnBackPressedDispatcher(...) { fun addCallback(onBackPressedCallback: OnBackPressedCallback) { eventDispatcher.addCallback(onBackPressedCallback.eventCallback) } private fun onBackStarted(backEvent: BackEventCompat) { eventDispatcher.dispatchOnStarted(backEvent.toNavigationEvent()) } private fun onBackProgressed(backEvent: BackEventCompat) { eventDispatcher.dispatchOnProgressed(backEvent.toNavigationEvent()) } ... } OnBackPressedDispatcher

Slide 169

Slide 169 text

class OnBackPressedDispatcher(...) { fun addCallback(onBackPressedCallback: OnBackPressedCallback) { eventDispatcher.addCallback(onBackPressedCallback.eventCallback) } private fun onBackStarted(backEvent: BackEventCompat) { eventDispatcher.dispatchOnStarted(backEvent.toNavigationEvent()) } private fun onBackProgressed(backEvent: BackEventCompat) { eventDispatcher.dispatchOnProgressed(backEvent.toNavigationEvent()) } ... } OnBackPressedDispatcher

Slide 170

Slide 170 text

class OnBackPressedDispatcher(...) { fun addCallback( owner: LifecycleOwner, onBackPressedCallback: OnBackPressedCallback ) { ... } ... } OnBackPressedDispatcher

Slide 171

Slide 171 text

Navigation 3 Integration

Slide 172

Slide 172 text

Navigation 3 Integration

Slide 173

Slide 173 text

Navigation 3 Integration

Slide 174

Slide 174 text

Navigation 3 Integration

Slide 175

Slide 175 text

What’s next for Navigation Event?

Slide 176

Slide 176 text

● System Back ○ Predictive Back What’s next for Navigation Event?

Slide 177

Slide 177 text

● System Back ○ Predictive Back ● Keyboard Events What’s next for Navigation Event? ● System Back ○ Predictive Back

Slide 178

Slide 178 text

● System Back ○ Predictive Back ● Keyboard Events ● Forward Navigation What’s next for Navigation Event? ● System Back ○ Predictive Back ● Keyboard Events

Slide 179

Slide 179 text

Proprietary + Confidential 04 Review

Slide 180

Slide 180 text

No content

Slide 181

Slide 181 text

✅ Keeps existing behavior and code that’s already tested. ✅ Can move to KMP step-by-step, reducing risk. ✅ Customers don't need to change their code. ❌ Harder to clean up old or platform-specific design. ❌ May involve complex refactoring and workarounds. Migrate a Library

Slide 182

Slide 182 text

✅ Keeps existing behavior and code that’s already tested. ✅ Can move to KMP step-by-step, reducing risk. ✅ Customers don't need to change their code. ❌ Harder to clean up old or platform-specific design. ❌ May involve complex refactoring and workarounds. Migrate a Library ✅ Clean design from the start, no legacy "platform-specific" code. ✅ Easier to apply modern best practices. ❌ Slower to build feature parity if the existing library is large. ❌ May miss edge cases already handled in the existing code. ❌ Customers will need to migrate. Rewrite a Library

Slide 183

Slide 183 text

✅ Keeps existing behavior and code that’s already tested. ✅ Can move to KMP step-by-step, reducing risk. ✅ Customers don't need to change their code. ❌ Harder to clean up old or platform-specific design. ❌ May involve complex refactoring and workarounds. Migrate a Library ✅ Clean design from the start, no legacy "platform-specific" code. ✅ Easier to apply modern best practices. ❌ Slower to build feature parity if the existing library is large. ❌ May miss edge cases already handled in the existing code. ❌ Customers will need to migrate. Rewrite a Library

Slide 184

Slide 184 text

● SavedState 1.3 (KMP + Serialization) ● NavigationEvent Alpha01 ● Navigation3 Alpha01 New Releases! goo.gle/nav3

Slide 185

Slide 185 text

Thank You, and Donʼt Forget to Vote linkedIn@jeremybwoods 🦋 @marcellogalhardo.dev

Slide 186

Slide 186 text

if (action == Action.Back) { val event = NavigationEvent(currentX, currentY, calculateProgress(startX, currentX, windowSize.width.toFloat()), Edge.Left) backChannel.onProgressed(event) } override fun dispatchEvent(e: NavigationEventWrapper, callback: NavigationEventCallback) { when (e.action) { Action.Back -> { when (e.type) { EventType.Started -> { backNavigation?.let { it.callback.onBackCancelled() } backNavigation = Navigation(callback.deepCopy()).also { it.callback.onBackStarted(e.externalEvent!!) } } EventType.Progressed -> backNavigation?.callback?.onBackProgressed(e.externalEvent!!) EventType.Cancelled -> { backNavigation?.callback?.onBackCancelled() backNavigation = null } EventType.Completed -> { backNavigation?.callback?.onBackCompleted() backNavigation = null } } } } }