Safe Compose arguments: An improved way to navigate in Jetpack Compose — Part 2

This is the second part of the series. For Part 1, please read here: https://proandroiddev.com/safe-compose-arguments-an-improved-way-to-navigate-in-jetpack-compose-95c84722eec2
In my previous article, I created an annotation processor for creating the methods that will be used to navigate in Jetpack Compose. In this part, I am extending the annotation processor to generate code for parcelable, serializable, and list and array types. The basic principle will remain the same, figure out the type of the argument and generate corresponding code accordingly.
First, let’s explore how you would pass a parcelable object in Jetpack Compose. Let’s say we have a class of type User
, containing an id and a name, that we would like to pass as an argument. Following is an example of a class containing id
and name
. It is also implementing the Parcelable
interface.
Now to actually pass this in Jetpack Compose, we would have to create our own custom NavType
that will be used by compose internally to put our User type object into a bundle, and later retrieve it.
To do this, we override the androidx.navigation.NavType
class in the following manner:
Then we can include our custom NavType in the argument list as follows:
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.
To get back our User
object, we need to parse its value from the bundle received in the back stack entry.
Now done 😅. To automate this process, our annotation processor needs to do the following-
- Determine the type of argument
- Generate its custom navigation type (
NavType
) - Parse back the argument from the bundle that is received in the back stack entry
The following section details out how the annotation processor performs the aforementioned steps. For full implementation, please check the complete module on GitHub as the article only contains excerpts to explain the concepts.
Where magic happens
In the previous article, the argument was being resolved to its class in a lot of places. To streamline this process, I have created a set of argument types that can be used to check the argument type.
To determine the type of argument, we have to check a slew of conditions. Primitive types are easy to determine since their resolved class name is of the form Boolean, Int, String...
. After we determine our argument is not of the primitive type, we have to do the following steps.
- Once we determine that the argument is of type
ArrayList
, we have to perform several checks. Whether the ArrayList type is parcelable or is serializable. This can be checked by checking the supertypes of the ArrayList template type (supertypes refers to the superclasses, the classes/interfaces it extends). - If we determine that our object is not an ArrayList, but rather a simple object, then we need to check for parcelable/serializable types.
- If none of the properties satisfies our condition, that means it cannot be put into a bundle, and we throw an error during compilation.
The following code demonstrates how we can determine the argument type.
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.
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.
Then we generate our get
and put
methods, that will, as the name suggests, get and put the value from the bundle.
Finally, we generate our parse method, that will convert the custom objects into strings using Gson
.
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.
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.
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
.
That’s it! Now the annotation processor will handle all the types of arguments that can be put into a bundle.
If you also want to develop such awesome things, come work at ShareChat. Apply to the relevant position below.
https://sharechat.hire.trakstar.com/
Links
Part 3 is here 🎉- (guide to integrate this library in your own project)
Github repo-