Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nav): impl ParcelableNavType #18

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.github.ryunen344.suburi.navigation

import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.BundleCompat
import androidx.navigation.NavType
import io.github.ryunen344.suburi.util.parcel
import io.github.ryunen344.suburi.util.unparcel
import timber.log.Timber
import java.util.Base64
import kotlin.reflect.KType
import kotlin.reflect.typeOf

@Suppress("FunctionName")
inline fun <reified T : Parcelable> ParcelableNavTypeMap(): Map<KType, ParcelableNavType<T>> =
mapOf(typeOf<T>() to ParcelableNavType<T>())

@Suppress("FunctionName")
inline fun <reified T : Parcelable> ParcelableNavTypeMap(
noinline onParseValue: ((String) -> T?),
): Map<KType, ParcelableNavType<T>> = mapOf(typeOf<T>() to ParcelableNavType<T>(onParseValue))

inline fun <reified T : Parcelable> ParcelableNavType() = ParcelableNavType(T::class.java)

inline fun <reified T : Parcelable> ParcelableNavType(noinline onParseValue: ((String) -> T?)) =
ParcelableNavType(T::class.java, onParseValue)

class ParcelableNavType<T : Parcelable>(
val clazz: Class<T>,
private val onParseValue: ((String) -> T?)? = null,
) : NavType<T>(false) {

override val name: String = clazz.name

override fun put(bundle: Bundle, key: String, value: T) {
bundle.putParcelable(key, value)
}

override fun get(bundle: Bundle, key: String): T? {
return BundleCompat.getParcelable(bundle, key, clazz)
}

override fun parseValue(value: String): T {
Timber.d("parseValue $value")
return onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).unparcel(clazz)
}

override fun serializeAsValue(value: T): String {
Timber.d("serializeAsValue $value")
return Base64.getUrlEncoder().encodeToString(value.parcel())
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ParcelableNavType<*>

if (clazz != other.clazz) return false
if (onParseValue != other.onParseValue) return false
if (name != other.name) return false

return true
}

override fun hashCode(): Int {
var result = clazz.hashCode()
result = 31 * result + (onParseValue?.hashCode() ?: 0)
result = 31 * result + name.hashCode()
return result
}
}

@Suppress("FunctionName")
inline fun <reified T : Parcelable> NullableParcelableNavTypeMap(): Map<KType, NullableParcelableNavType<T>> =
mapOf(typeOf<T>() to NullableParcelableNavType<T>())

@Suppress("FunctionName")
inline fun <reified T : Parcelable> NullableParcelableNavTypeMap(
noinline onParseValue: ((String) -> T?),
): Map<KType, NullableParcelableNavType<T>> = mapOf(typeOf<T>() to NullableParcelableNavType<T>(onParseValue))

inline fun <reified T : Parcelable> NullableParcelableNavType() = NullableParcelableNavType(T::class.java)

inline fun <reified T : Parcelable> NullableParcelableNavType(noinline onParseValue: ((String) -> T?)) =
NullableParcelableNavType(T::class.java, onParseValue)

class NullableParcelableNavType<T : Parcelable>(
val clazz: Class<T>,
private val onParseValue: ((String) -> T?)? = null,
) : NavType<T?>(true) {

override val name: String = clazz.name

override fun put(bundle: Bundle, key: String, value: T?) {
bundle.putParcelable(key, value)
}

override fun get(bundle: Bundle, key: String): T? {
return BundleCompat.getParcelable(bundle, key, clazz)
}

override fun parseValue(value: String): T? {
Timber.d("parseValue $value")
return if (value == "null") null else onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).unparcel(clazz)
}

override fun serializeAsValue(value: T?): String {
Timber.d("serializeAsValue $value")
return if (value == null) {
"null"
} else {
Base64.getUrlEncoder().encodeToString(value.parcel())
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as NullableParcelableNavType<*>

if (clazz != other.clazz) return false
if (onParseValue != other.onParseValue) return false
if (name != other.name) return false

return true
}

override fun hashCode(): Int {
var result = clazz.hashCode()
result = 31 * result + (onParseValue?.hashCode() ?: 0)
result = 31 * result + name.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ class SerializableNavType<T : java.io.Serializable>(
private val onParseValue: ((String) -> T?)? = null,
) : NavType<T>(false) {

override val name: String
get() = clazz.name
override val name: String = clazz.name

override fun put(bundle: Bundle, key: String, value: T) {
bundle.putSerializable(key, value)
Expand Down Expand Up @@ -58,13 +57,15 @@ class SerializableNavType<T : java.io.Serializable>(

if (clazz != other.clazz) return false
if (onParseValue != other.onParseValue) return false
if (name != other.name) return false

return true
}

override fun hashCode(): Int {
var result = clazz.hashCode()
result = 31 * result + (onParseValue?.hashCode() ?: 0)
result = 31 * result + name.hashCode()
return result
}
}
Expand All @@ -88,8 +89,7 @@ class NullableSerializableNavType<T : java.io.Serializable>(
private val onParseValue: ((String) -> T?)? = null,
) : NavType<T?>(true) {

override val name: String
get() = clazz.name
override val name: String = clazz.name

override fun put(bundle: Bundle, key: String, value: T?) {
bundle.putSerializable(key, value)
Expand All @@ -100,6 +100,7 @@ class NullableSerializableNavType<T : java.io.Serializable>(
}

override fun parseValue(value: String): T? {
Timber.d("parseValue $value")
return if (value == "null") null else onParseValue?.invoke(value) ?: Base64.getUrlDecoder().decode(value).deserialize()
}

Expand All @@ -120,13 +121,15 @@ class NullableSerializableNavType<T : java.io.Serializable>(

if (clazz != other.clazz) return false
if (onParseValue != other.onParseValue) return false
if (name != other.name) return false

return true
}

override fun hashCode(): Int {
var result = clazz.hashCode()
result = 31 * result + (onParseValue?.hashCode() ?: 0)
result = 31 * result + name.hashCode()
return result
}
}
16 changes: 10 additions & 6 deletions app/src/main/java/io/github/ryunen344/suburi/ui/screen/Routes.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.ryunen344.suburi.ui.screen

import android.os.Bundle
import android.os.Parcelable
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
Expand All @@ -18,7 +19,8 @@ import androidx.navigation.get
import androidx.navigation.internalToRoute
import androidx.navigation.navDeepLink
import androidx.navigation.serialization.decodeArguments
import io.github.ryunen344.suburi.navigation.SerializableNavTypeMap
import io.github.ryunen344.suburi.navigation.ParcelableNavTypeMap
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
Expand Down Expand Up @@ -58,8 +60,8 @@ sealed class Routes {
inline val <reified T : Routes> KClass<T>.typeMap: Map<KType, @JvmSuppressWildcards NavType<*>>
get() = when (this) {
Routes.Top::class -> emptyMap()
Routes.Uuid::class -> SerializableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse)
Routes.Structures::class -> SerializableNavTypeMap<Structure>()
Routes.Uuid::class -> ParcelableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse)
Routes.Structures::class -> ParcelableNavTypeMap<Structure>()
else -> error("unexpected type parameter")
}

Expand All @@ -69,19 +71,21 @@ val <T : Routes> KClass<T>.deepLinks: List<NavDeepLink>
Routes.Uuid::class -> listOf(
navDeepLink<Routes.Uuid>(
basePath = "https://www.example.com/uuid",
typeMap = SerializableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse),
typeMap = ParcelableNavTypeMap<WrappedUuid>(onParseValue = onWrappedUuidParse),
),
)

Routes.Structures::class -> emptyList()
else -> error("unexpected type parameter")
}

@Parcelize
@Serializable
data class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable
class WrappedUuid(@Serializable(with = UUIDSerializer::class) val value: UUID) : java.io.Serializable, Parcelable

@Parcelize
@Serializable
data class Structure(val value1: String, val value2: Long, val value3: String) : java.io.Serializable {
class Structure(val value1: String, val value2: Long, val value3: String) : java.io.Serializable, Parcelable {
companion object {
fun random(): Structure {
return Structure(
Expand Down
52 changes: 52 additions & 0 deletions app/src/main/java/io/github/ryunen344/suburi/util/Parcelable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.github.ryunen344.suburi.util

import android.os.Parcel
import android.os.Parcelable
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@Suppress("TooGenericExceptionCaught")
inline fun <T : Parcel?, R> T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
this.recycleFinally(exception)
}
}

@Suppress("TooGenericExceptionCaught")
fun Parcel?.recycleFinally(cause: Throwable?): Unit = when {
this == null -> {}
cause == null -> recycle()
else ->
try {
recycle()
} catch (closeException: Throwable) {
cause.addSuppressed(closeException)
}
}

fun Parcelable.parcel(): ByteArray {
return Parcel.obtain().use { parcel ->
writeToParcel(parcel, Parcelable.PARCELABLE_WRITE_RETURN_VALUE)
parcel.marshall()
}
}

@Suppress("UNCHECKED_CAST")
fun <T : Parcelable> ByteArray.unparcel(clazz: Class<T>): T {
val creator = clazz.getDeclaredField("CREATOR").get(null) as? Parcelable.Creator<T>
?: throw IllegalArgumentException("Could not access CREATOR field in class ${clazz.simpleName}")
return Parcel.obtain().use { parcel ->
parcel.unmarshall(this, 0, this.size)
parcel.setDataPosition(0)
creator.createFromParcel(parcel)
}
}