diff --git a/core/api/core.api b/core/api/core.api index 95532a0..080d570 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -36,3 +36,7 @@ public final class com/kiwi/navigationcompose/typed/RoutingKt { public static final fun toDeepLinkUri (Lcom/kiwi/navigationcompose/typed/Destination;)Landroid/net/Uri; } +public final class com/kiwi/navigationcompose/typed/internal/NavBuilderKt { + public static final fun decodeArguments (Landroidx/lifecycle/SavedStateHandle;Lkotlinx/serialization/KSerializer;)Lcom/kiwi/navigationcompose/typed/Destination; +} + diff --git a/core/src/main/kotlin/com/kiwi/navigationcompose/typed/NavBuilder.kt b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/NavBuilder.kt index 62699f9..72f25de 100644 --- a/core/src/main/kotlin/com/kiwi/navigationcompose/typed/NavBuilder.kt +++ b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/NavBuilder.kt @@ -1,11 +1,12 @@ package com.kiwi.navigationcompose.typed -import android.os.Bundle import androidx.annotation.MainThread import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable +import androidx.core.os.bundleOf +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink @@ -15,7 +16,9 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.dialog import androidx.navigation.compose.navigation import androidx.navigation.navArgument -import com.kiwi.navigationcompose.typed.internal.UriBundleDecoder +import com.kiwi.navigationcompose.typed.internal.BundleDataMap +import com.kiwi.navigationcompose.typed.internal.UriDataDecoder +import com.kiwi.navigationcompose.typed.internal.decodeArguments import com.kiwi.navigationcompose.typed.internal.isNavTypeOptional import kotlin.reflect.KClass import kotlinx.serialization.ExperimentalSerializationApi @@ -265,10 +268,14 @@ public fun decodeArguments( ): T { // Arguments may be empty if the destination does not have any parameters, // and it is a start destination. - val decoder = UriBundleDecoder(navBackStackEntry.arguments ?: Bundle()) + val decoder = UriDataDecoder(BundleDataMap(navBackStackEntry.arguments ?: bundleOf())) return decoder.decodeSerializableValue(serializer) } +@ExperimentalSerializationApi +public inline fun SavedStateHandle.decodeArguments(): T = + this.decodeArguments(serializer()) + @Deprecated( "Deprecated in favor of navigation builder that supports AnimatedContent", level = DeprecationLevel.HIDDEN, diff --git a/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/NavBuilder.kt b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/NavBuilder.kt new file mode 100644 index 0000000..4399c89 --- /dev/null +++ b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/NavBuilder.kt @@ -0,0 +1,15 @@ +package com.kiwi.navigationcompose.typed.internal + +import androidx.lifecycle.SavedStateHandle +import com.kiwi.navigationcompose.typed.Destination +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer + +@ExperimentalSerializationApi +@PublishedApi +internal fun SavedStateHandle.decodeArguments( + serializer: KSerializer, +): T { + val decoder = UriDataDecoder(SavedStateDataMap(this)) + return decoder.decodeSerializableValue(serializer) +} diff --git a/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriBundleDecoder.kt b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataDecoder.kt similarity index 62% rename from core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriBundleDecoder.kt rename to core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataDecoder.kt index 553fc85..5dbdc7d 100644 --- a/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriBundleDecoder.kt +++ b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataDecoder.kt @@ -1,6 +1,5 @@ package com.kiwi.navigationcompose.typed.internal -import android.os.Bundle import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor @@ -10,13 +9,13 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule @ExperimentalSerializationApi -internal class UriBundleDecoder( - private val bundle: Bundle, +internal class UriDataDecoder( + private val data: UriDataMap, ) : AbstractDecoder() { override val serializersModule: SerializersModule by lazy { getSerializersModule() } private val json by lazy { - Json { serializersModule = this@UriBundleDecoder.serializersModule } + Json { serializersModule = this@UriDataDecoder.serializersModule } } private var root = true @@ -36,7 +35,7 @@ internal class UriBundleDecoder( while (elementIndex < elementsCount) { elementName = descriptor.getElementName(elementIndex) elementIndex++ - if (bundle.containsKey(elementName)) { + if (data.contains(elementName)) { return elementIndex - 1 } } @@ -44,24 +43,24 @@ internal class UriBundleDecoder( } override fun decodeNotNullMark(): Boolean = - bundle.containsKey(elementName) && bundle.getString(elementName) != null + data.contains(elementName) && data.get(elementName) != null override fun decodeNull(): Nothing? = null // natively supported - override fun decodeInt(): Int = bundle.getString(elementName)!!.toInt() - override fun decodeLong(): Long = bundle.getString(elementName)!!.toLong() - override fun decodeFloat(): Float = bundle.getString(elementName)!!.toFloat() - override fun decodeBoolean(): Boolean = bundle.getString(elementName)!!.toBooleanStrict() - override fun decodeString(): String = bundle.getString(elementName)!! + override fun decodeInt(): Int = data.get(elementName)!!.toInt() + override fun decodeLong(): Long = data.get(elementName)!!.toLong() + override fun decodeFloat(): Float = data.get(elementName)!!.toFloat() + override fun decodeBoolean(): Boolean = data.get(elementName)!!.toBooleanStrict() + override fun decodeString(): String = data.get(elementName)!! // delegated to other primitives - override fun decodeDouble(): Double = bundle.getString(elementName)!!.toDouble() - override fun decodeByte(): Byte = bundle.getString(elementName)!!.toByte() - override fun decodeShort(): Short = bundle.getString(elementName)!!.toShort() - override fun decodeChar(): Char = bundle.getString(elementName)!![0] + override fun decodeDouble(): Double = data.get(elementName)!!.toDouble() + override fun decodeByte(): Byte = data.get(elementName)!!.toByte() + override fun decodeShort(): Short = data.get(elementName)!!.toShort() + override fun decodeChar(): Char = data.get(elementName)!![0] override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = decodeInt() diff --git a/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataMap.kt b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataMap.kt new file mode 100644 index 0000000..fdd1a9c --- /dev/null +++ b/core/src/main/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataMap.kt @@ -0,0 +1,23 @@ +package com.kiwi.navigationcompose.typed.internal + +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle + +internal interface UriDataMap { + fun contains(key: String): Boolean + fun get(key: String): String? +} + +internal class BundleDataMap( + private val bundle: Bundle, +) : UriDataMap { + override fun contains(key: String): Boolean = bundle.containsKey(key) + override fun get(key: String): String? = bundle.getString(key) +} + +internal class SavedStateDataMap( + private val savedStateHandle: SavedStateHandle, +) : UriDataMap { + override fun contains(key: String): Boolean = savedStateHandle.contains(key) + override fun get(key: String): String? = savedStateHandle[key] +} diff --git a/core/src/test/kotlin/com/kiwi/navigationcompose/typed/internal/UriBundleDecoderTest.kt b/core/src/test/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataDecoderTest.kt similarity index 95% rename from core/src/test/kotlin/com/kiwi/navigationcompose/typed/internal/UriBundleDecoderTest.kt rename to core/src/test/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataDecoderTest.kt index fb1d49b..a156054 100644 --- a/core/src/test/kotlin/com/kiwi/navigationcompose/typed/internal/UriBundleDecoderTest.kt +++ b/core/src/test/kotlin/com/kiwi/navigationcompose/typed/internal/UriDataDecoderTest.kt @@ -16,7 +16,7 @@ import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalSerializationApi::class) @RunWith(RobolectricTestRunner::class) -internal class UriBundleDecoderTest { +internal class UriDataDecoderTest { @Suppress("unused") @Serializable class TestData( @@ -53,7 +53,7 @@ internal class UriBundleDecoderTest { "o" to "\"14\"", ) - val decoder = UriBundleDecoder(bundle) + val decoder = UriDataDecoder(BundleDataMap(bundle)) val data = decoder.decodeSerializableValue(serializer()) Assert.assertEquals(1, data.a) diff --git a/demo/src/main/kotlin/com/kiwi/navigationcompose/typed/demo/screens/Demo.kt b/demo/src/main/kotlin/com/kiwi/navigationcompose/typed/demo/screens/Demo.kt index ee47c9d..fb7ed57 100644 --- a/demo/src/main/kotlin/com/kiwi/navigationcompose/typed/demo/screens/Demo.kt +++ b/demo/src/main/kotlin/com/kiwi/navigationcompose/typed/demo/screens/Demo.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -14,7 +15,14 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.viewmodel.viewModelFactory +import com.kiwi.navigationcompose.typed.decodeArguments import com.kiwi.navigationcompose.typed.demo.HomeDestinations +import kotlinx.serialization.ExperimentalSerializationApi @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -22,6 +30,7 @@ internal fun Demo( args: HomeDestinations.Demo, onNavigateUp: () -> Unit, ) { + val viewModel = viewModel(factory = ViewModelFactory) Scaffold( topBar = { TopAppBar( @@ -43,6 +52,21 @@ internal fun Demo( Text( text = args.toString(), ) + Divider() + Text( + text = viewModel.arguments.toString(), + ) } } } + +internal val ViewModelFactory = viewModelFactory { + addInitializer(StateDemoViewModel::class) { StateDemoViewModel(createSavedStateHandle()) } +} + +internal class StateDemoViewModel( + state: SavedStateHandle, +) : ViewModel() { + @OptIn(ExperimentalSerializationApi::class) + val arguments = state.decodeArguments() +} diff --git a/readme.md b/readme.md index 866fe66..d3bf4d7 100644 --- a/readme.md +++ b/readme.md @@ -111,6 +111,36 @@ private fun Home( } ``` +### ViewModel + +You can pass your destination arguments directly from the UI using parameters/the assisted inject feature. + +For example, in Koin: + +```kotlin +val KoinModule = module { + viewModelOf(::DemoViewModel) +} + +fun DemoScreen(arguments: HomeDestinations.Demo) { + val viewModel = getViewModel { parametersOf(arguments) } +} + +class DemoViewModel( + arguments: HomeDestinations.Demo, +) +``` + +Alternatively, you can read your destination from a `SavedStateHandle` instance: + +```kotlin +class DemoViewModel( + state: SavedStateHandle, +) : ViewModel() { + val arguments = state.decodeArguments() +} +``` + ### Extensibility What about cooperation with Accompanist's Material `bottomSheet {}` integration? Do not worry. Basically, all the functionality is just a few simple functions. Create your own abstraction and use `createRoutePattern()`, `createNavArguments()`, `decodeArguments()` and `registerDestinationType()` functions.