Skip to content

Commit

Permalink
add SavedStateHandle.decodeArguments() extension
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Nov 27, 2023
1 parent 27f91ed commit 0a4b1af
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 20 deletions.
4 changes: 4 additions & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -265,10 +268,14 @@ public fun <T : Destination> 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 <reified T : Destination> SavedStateHandle.decodeArguments(): T =
this.decodeArguments(serializer())

@Deprecated(
"Deprecated in favor of navigation builder that supports AnimatedContent",
level = DeprecationLevel.HIDDEN,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T : Destination> SavedStateHandle.decodeArguments(
serializer: KSerializer<T>,
): T {
val decoder = UriDataDecoder(SavedStateDataMap(this))
return decoder.decodeSerializableValue(serializer)
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -36,32 +35,32 @@ internal class UriBundleDecoder(
while (elementIndex < elementsCount) {
elementName = descriptor.getElementName(elementIndex)
elementIndex++
if (bundle.containsKey(elementName)) {
if (data.contains(elementName)) {
return elementIndex - 1
}
}
return CompositeDecoder.DECODE_DONE
}

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()

Expand Down
Original file line number Diff line number Diff line change
@@ -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]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -53,7 +53,7 @@ internal class UriBundleDecoderTest {
"o" to "\"14\"",
)

val decoder = UriBundleDecoder(bundle)
val decoder = UriDataDecoder(BundleDataMap(bundle))
val data = decoder.decodeSerializableValue(serializer<TestData>())

Assert.assertEquals(1, data.a)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,14 +15,22 @@ 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
internal fun Demo(
args: HomeDestinations.Demo,
onNavigateUp: () -> Unit,
) {
val viewModel = viewModel<StateDemoViewModel>(factory = ViewModelFactory)
Scaffold(
topBar = {
TopAppBar(
Expand All @@ -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<HomeDestinations.Demo>()
}
30 changes: 30 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<DemoViewModel> { 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<HomeDestinations.Demo>()
}
```

### 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.
Expand Down

0 comments on commit 0a4b1af

Please sign in to comment.