xxfast / Decompose-Router

A Compose-multiplatform navigation library that leverage Decompose to create an API inspired by Conductor
https://xxfast.github.io/Decompose-Router/
221 stars 9 forks source link

Support complex objects with external classes that don't extend Parcelize #21

Closed Deaths-Door closed 1 year ago

Deaths-Door commented 1 year ago

Issue Description

I encountered an issue while using the Decompose-Router library where passing arguments using the @Parcelize annotation is not supported for complex objects with external classes that don't extend Parcelize.

@Parcelize
sealed class Other : Route {
    class DisplayAlbumDetails(val album: Album) : Other() {
        @Composable
        override fun content(vararg args: Any) = ComposableDisplayAlbumDetails(album)
    }
}

Where Album is a complex object containing external classes that don't extend Parcelize.

@Serializable
data class Album(
    val id : UUID = UUID.random(),
    val name : String,
    val image : String? = null,
    val songs : MutableList<MediaItem> = mutableListOf(),
    val isPinned : Boolean = false
)

I've considered using the @RawValue annotation, but that is specific to Android and not suitable for this scenario. Additionally, using expect/actual declarations would defeat the purpose of using the Decompose-Router library.

I suggest two potential solutions:

xxfast commented 1 year ago

This is an Android limitation. You can only save/restore primitives and Parcelables (and there's also an 50K size limit)

However, if you wish to parcel an external type - say UUID - you must write a custom parceler for this. Instructions for that is documented here, but the gist is

// commonMain
expect object UuidParceler : Parceler<UUID>

// androidMain
actual object UuidParceler : Parceler<UUID> {
    // I'm not sure what the exact API to create a uuid from string, but hoping something like this
    override fun create(parcel: Parcel): UUID = UUID(parcel.readString())

    override fun UUID.write(parcel: Parcel, flags: Int) {
        parcel.writeString(this.toString()) 
    }
}

Then on state declaration

@Parcelize
@TypeParceler<UUID, UuidParceler>()
data class Album(
    val id : UUID = UUID.random(),
    val name : String,
    val image : String? = null,
    val songs : MutableList<MediaItem> = mutableListOf(),
    val isPinned : Boolean = false
): Parcelable 

Also, due to the Android bundle size limitation (of 50K) - I strongly advise limiting the amount of data that goes into the state. Especially with a MutableList<MediaItem>, parceling a Album can easily blow up more than 50k

Deaths-Door commented 1 year ago

Yes, you're right. While it's true that Android imposes a limitation where you can only save and restore primitives and Parcelables. Serializing complex objects into a string representation allows for easier usage and handling of those objects. It provides a more flexible approach, enabling the storage and retrieval of complex data structures.. A potential solution is to serialize the object into a string representation and then deserialize it back into an object. By utilizing KClazz and handling the serialization process, this approach can be more effective and efficient. Maybe something like this ..

data class Other(
     val id : UUID = UUID.random()
) : Parcelable  // extending parcelable as router accepts this type .

Then I'm not sure how it handles the checking internally so maybe just add a check

inline fun <refied T>checkOrThrowError(clazz : KClazz<T>) {
     if(clazz has serializer) //add flag to serialize and deserialize it instead
     else // default checking 
}

It opens up possibilities for easily integrating object serialization and deserialization into their codebase, without having to manually handle the conversion of complex objects + external classes.

xxfast commented 1 year ago

This goes a little over the scope of this library but generally, Decompose-Router will always delegate state management to the underlying platform.

There are also other considerations, like

While I agree, it is cumbersome to make states extend existing parcelisation implementations - on Android, this is the only native way to achieve state restoration unfortunately.

arkivanov commented 1 year ago

Just wanted to say that the bundle limit is ~500k (not 50k) on Android, shared across the whole app's process.