Blog Infos
Author
Published
Topics
,
Published
data class User(
val id: Int,
val name: String
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString() ?: ""
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(id)
parcel.writeString(name)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<User> {
override fun createFromParcel(parcel: Parcel): User {
return User(parcel)
}
override fun newArray(size: Int): Array<User?> {
return arrayOfNulls(size)
}
}
}
view raw demo.kt hosted with ❤ by GitHub
val UserPage_UniqueUserNavType: NavType<User> = object : NavType<User>(false) {
override val name: String
get() = "uniqueUser"
override fun get(bundle: Bundle, key: String): .User? {
return bundle.getParcelable(key)
}
override fun parseValue(value: String): User {
return gson.fromJson(value, object : TypeToken<User>() {}.type)
}
override fun put(bundle: Bundle, key: String, value: User) {
bundle.putParcelable(key, value)
}
}
view raw demo.kt hosted with ❤ by GitHub
composable(
route = "userPage?uniqueUser={uniqueUser}",
arguments = mutableListOf(
navArgument("uniqueUser") {
type = UserPage_UniqueUsersNavType
}
) { backStackEntry ->
// content
}
)
view raw demo.kt hosted with ❤ by GitHub

Work done? Not yet. To pass an instance of User in Jetpack Compose, we need to somehow convert it to a valid string that can be parsed back into an object containing the same data. This is achieved using Gson. We need to convert our object to a JSON representation, and then encode it using Uri . The following example shows how we can achieve this.

navHostController.navigate("userPage?uniqueUser=${Uri.encode(gson.toJson(uniqueUser))}")
view raw demo.kt hosted with ❤ by GitHub

To get back our User object, we need to parse its value from the bundle received in the back stack entry.

backStackEntry.arguments?.getParcelable<User>("uniqueUser")
view raw demo.kt hosted with ❤ by GitHub

safe-compose-args/compose-annotation-processor/src/main/java/com/example/compose

Where magic happens
enum class ComposeArgumentType {
INT,
LONG,
FLOAT,
BOOLEAN,
STRING,
INT_ARRAY,
LONG_ARRAY,
FLOAT_ARRAY,
BOOLEAN_ARRAY,
PARCELABLE,
PARCELABLE_ARRAY,
SERIALIZABLE
}
view raw demo.kt hosted with ❤ by GitHub
composeArgumentType = when (resolvedClassDeclarationName) {
"Boolean" -> ComposeArgumentType.BOOLEAN
"String" -> ComposeArgumentType.STRING
"Float" -> ComposeArgumentType.FLOAT
"Int" -> ComposeArgumentType.INT
"Long" -> ComposeArgumentType.LONG
"IntArray" -> ComposeArgumentType.INT_ARRAY
"BooleanArray" -> ComposeArgumentType.BOOLEAN_ARRAY
"LongArray" -> ComposeArgumentType.LONG_ARRAY
"FloatArray" -> ComposeArgumentType.FLOAT_ARRAY
else -> when {
resolvedClassQualifiedName == "kotlin.collections.ArrayList" -> {
var isParcelable = false
var isSerializable = false
for (argument in typeArguments) {
val resolvedArgument = argument.type?.resolve()
if ((resolvedArgument?.declaration as? KSClassDeclaration)?.superTypes?.map { it.toString() }
?.contains("Parcelable") == true) {
isParcelable = true
}
if ((resolvedArgument?.declaration as? KSClassDeclaration)?.superTypes?.map { it.toString() }
?.contains("Serializable") == true) {
isSerializable = true
}
}
if (isParcelable) {
ComposeArgumentType.PARCELABLE_ARRAY
} else if (isSerializable) {
ComposeArgumentType.SERIALIZABLE
} else {
logger.error(
"invalid property type, cannot pass it in bundle",
property
)
return null
}
}
(resolvedType.declaration as KSClassDeclaration).superTypes.map { it.toString() }
.contains("Parcelable") -> {
ComposeArgumentType.PARCELABLE
}
(resolvedType.declaration as KSClassDeclaration).superTypes.map { it.toString() }
.contains("Serializable") -> {
ComposeArgumentType.SERIALIZABLE
}
else -> {
logger.error(
"invalid property type, cannot pass it in bundle",
property
)
return null
}
}
}
view raw demo.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Navigation superpowers at your fingertips

This talk will begin by the demonstration of a beautiful sample app built with Compose Mulitplatform and Appyx, complete with:
Watch Video

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android engineer
Bumble

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android en ...
Bumble

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android enginee ...
Bumble

Jobs

Once we determine our argument type, we can proceed further and generate custom NavTypes for the ArrayList objects, parcelables and serializables. We loop over our arguments for each destination, and only take those arguments which are of the aforementioned types.

if (!(propertyInfo.composeArgumentType == ComposeArgumentType.PARCELABLE ||
propertyInfo.composeArgumentType == ComposeArgumentType.PARCELABLE_ARRAY ||
propertyInfo.composeArgumentType == ComposeArgumentType.SERIALIZABLE
)) {
return@forEach
}
view raw demo.kt hosted with ❤ by GitHub

Then we generate the variable definition for our custom nav type and override the name field to provide our own name field. The code is pretty self-explanatory.

file addLine "val ${className}_${propertyInfo.propertyName.replaceFirstChar { it.uppercase() }}NavType: NavType<"
addVariableType(file, propertyInfo)
file addPhrase "> = object : NavType<"
addVariableType(file, propertyInfo)
file addPhrase ">(false) {"
tabs++
file addLine "override val name: String"
tabs++
file addLine "get() = "
file addPhrase "\"${propertyInfo.propertyName}\""
tabs--
view raw demo.kt hosted with ❤ by GitHub

Then we generate our get and put methods, that will, as the name suggests, get and put the value from the bundle.

file addLine "override fun get(bundle: Bundle, key: String): "
addVariableType(file, propertyInfo)
file addPhrase "? {"
tabs++
when (propertyInfo.composeArgumentType) {
ComposeArgumentType.PARCELABLE -> file addLine "return bundle.getParcelable(key)"
ComposeArgumentType.PARCELABLE_ARRAY -> file addLine "return bundle.getParcelableArrayList(key)"
ComposeArgumentType.SERIALIZABLE -> {
file addLine "return bundle.getSerializable(key) as? "
addVariableType(file, propertyInfo)
}
}
tabs--
file addLine "}"
file addLine "override fun put(bundle: Bundle, key: String, value: "
addVariableType(file, propertyInfo)
file addPhrase ") {"
tabs++
when (propertyInfo.composeArgumentType) {
ComposeArgumentType.PARCELABLE -> file addLine "bundle.putParcelable(key, value)"
ComposeArgumentType.PARCELABLE_ARRAY -> file addLine "bundle.putParcelableArrayList(key, value)"
ComposeArgumentType.SERIALIZABLE -> file addLine "bundle.putSerializable(key, value)"
}
tabs--
file addLine "}"
view raw demo.kt hosted with ❤ by GitHub

Finally, we generate our parse method, that will convert the custom objects into strings using Gson.

file addLine "override fun parseValue(value: String): "
addVariableType(file, propertyInfo)
file addPhrase " {"
tabs++
file addLine "return gson.fromJson(value, object : TypeToken<"
addVariableType(file, propertyInfo)
file addPhrase ">() {}.type)"
tabs--
file addLine "}"
view raw demo.kt hosted with ❤ by GitHub

This code will generate custom navigation types for all the types of arguments that can be put into a bundle. These generated variables will not be scoped to a class, but to the entire module, so they will be accessible everywhere. To use these generated navigation types, we can simply specify the navigation type based on its class name and its variable name.

fun getElementNavType(): String {
return when (propertyInfo.composeArgumentType) {
ComposeArgumentType.BOOLEAN -> "NavType.BoolType"
ComposeArgumentType.STRING -> "NavType.StringType"
ComposeArgumentType.FLOAT -> "NavType.FloatType"
ComposeArgumentType.INT -> "NavType.IntType"
ComposeArgumentType.LONG -> "NavType.LongType"
ComposeArgumentType.INT_ARRAY -> "IntArrayType"
ComposeArgumentType.BOOLEAN_ARRAY -> "BoolArrayType"
ComposeArgumentType.FLOAT_ARRAY -> "FloatArrayType"
ComposeArgumentType.LONG_ARRAY -> "LongArrayType"
else -> {
"${className}_${propertyInfo.propertyName.replaceFirstChar { it.uppercase() }}NavType"
}
}
}
view raw demo.kt hosted with ❤ by GitHub

Similarly, to parse the argument back from the bundle, we can check again ComposeArgumentType and decide how the value should be parsed. The following code shows how this can be achieved.

fun getParsedElement() {
when (propertyInfo.composeArgumentType) {
ComposeArgumentType.BOOLEAN -> file addPhrase "backStackEntry.arguments?.getBoolean(\"$argumentName\") ?: false"
ComposeArgumentType.STRING -> file addPhrase "backStackEntry.arguments?.getString(\"$argumentName\") ?: \"\""
ComposeArgumentType.FLOAT -> file addPhrase "backStackEntry.arguments?.getFloat(\"$argumentName\") ?: 0F"
ComposeArgumentType.INT -> file addPhrase "backStackEntry.arguments?.getInt(\"$argumentName\") ?: 0"
ComposeArgumentType.LONG -> file addPhrase "backStackEntry.arguments?.getLong(\"$argumentName\") ?: 0L"
ComposeArgumentType.INT_ARRAY -> file addPhrase "backStackEntry.arguments?.getIntArray(\"$argumentName\") ?: intArrayOf()"
ComposeArgumentType.BOOLEAN_ARRAY -> file addPhrase "backStackEntry.arguments?.getBooleanArray(\"$argumentName\") ?: booleanArrayOf()"
ComposeArgumentType.LONG_ARRAY -> file addPhrase "backStackEntry.arguments?.getLongArray(\"$argumentName\") ?: longArrayOf()"
ComposeArgumentType.FLOAT_ARRAY -> file addPhrase "backStackEntry.arguments?.getFloatArray(\"$argumentName\") ?: floatArrayOf()"
ComposeArgumentType.PARCELABLE -> {
file addPhrase "backStackEntry.arguments?.getParcelable<"
addVariableType(file, propertyInfo)
file addPhrase ">(\"$argumentName\") ?: throw NullPointerException(\"parcel value not found\")"
}
ComposeArgumentType.PARCELABLE_ARRAY -> {
file addPhrase "backStackEntry.arguments?.getParcelableArrayList"
visitChildTypeArguments(propertyInfo.typeArguments)
file addPhrase "(\"$argumentName\")"
file addPhrase " ?: throw NullPointerException(\"parcel value not found\")"
}
ComposeArgumentType.SERIALIZABLE -> {
file addPhrase "backStackEntry.arguments?.getSerializable"
file addPhrase "(\"$argumentName\") as? "
addVariableType(file, propertyInfo)
file addPhrase " ?: throw NullPointerException(\"parcel value not found\")"
}
}
}
view raw demo.kt hosted with ❤ by GitHub

The last thing to do is the conversion of any type of variable into a string representation. This will be done when getting the route for our composable destination. Primitive types Int, Boolean, String, Long, Float can simply be converted using .toString(). For other custom types, we have to convert the value using Gson.

file addLine "return \"$route${if (propertyMap.isNotEmpty()) "?" else ""}\" + "
tabs++
tabs++
count = 0
properties.forEach { property ->
count++
val propertyInfo = propertyMap[property] ?: run {
logger.error("Invalid type argument", property)
return
}
val argumentName = propertyInfo.propertyName
file addLine "\"$argumentName="
file addPhrase when (propertyInfo.composeArgumentType) {
ComposeArgumentType.INT,
ComposeArgumentType.BOOLEAN,
ComposeArgumentType.LONG,
ComposeArgumentType.FLOAT,
ComposeArgumentType.STRING -> "$$argumentName"
else -> "\${Uri.encode(gson.toJson($argumentName))}"
}
if (count == propertyMap.size) {
file addPhrase "\""
} else {
file addPhrase ",\""
}
file addPhrase " + "
}
file addLine "\"\""
view raw demo.kt hosted with ❤ by GitHub

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu