Passing Data with Safe Args using Android Jetpack
Tags: android, kotlin, androidjetpack
Published: 2020-08-31 19:39:12

{% youtube PdtXBUG9R84 %}

Android Jetpack aims to make life easier by providing devs with several tools that help speed up development while encouraging best practices. The tools we're going to be focusing on today are the Navigation component and passing data with Safe Args.

Safe Args is a Gradle plugin that gives us type safety when navigating and passing data between destinations.

We will be exploring these concepts by creating a simple app that displays a list of users in a fragment that contains a `RecyclerView` and passing the selected `User` to our `UserDetailsFragment`.

![App Demo](https://www.kiloloco.com/images/passing-data-with-safe-args-demo.gif)

We'll start from scratch with a new AndroidX project with an Empty Activity.

![New Android Project with Empty Activity Configuration](https://www.kiloloco.com/images/passing-data-with-safe-args-new-project.png)

Let's start off by setting up our dependencies.

In your project `build.gradle` file, add the following:

```groovy
... // dependencies {

def nav_version = "2.3.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"

... // dependencies closing }
```

In the module `build.gradle` file, we will need all of the following:

```groovy
... // other plugins

apply plugin: "androidx.navigation.safeargs"

... // android {
```

This plugin will generate our `NavArgs` and `NavDirections` for us.

```groovy
... // android {

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
    jvmTarget = "1.8"
}

... // android closing }
```

Java 8 is a minimum requirement to work with safe args.

```groovy
... // depedencies {

def nav_version = "2.3.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

... // dependencies closing }
```

We will keep our Android Jetpack Navigation depedency versions in sync by using a single variable for all three dependencies.

We should be able to build our project, get all of our dependencies into the project, and get to the juicy stuff now 🥩

Our app needs to show a list of users, then be able to open the details screen for the selected user. Let's make two fragments that represent both of those screens.

We'll start by creating the `User` object that we will be working with. (`Right click project folder > New > Kotlin File/Class > Class`)

```kotlin
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class User(val id: Int, val name: String, val age: Int): Parcelable {
    val description: String
        get() = "$name is $age years old."
}
```

We are keeping our object very simple here by only giving a few properties like `id`, `name`, and `age`. We are also making it `Parcelable` so we can pass it between fragments.

Let's go ahead and add the fragment that will be responsible for showing our users (`Right click project folder > New > Fragment > Fragment (List)`)

![Users Fragment Configuration](https://www.kiloloco.com/images/passing-data-with-safe-args-list-fragment.png)

I'll configure the component with names that make sense for my project, in this case the names/objects will revolve around our `User` object.

Going through this process will generate a lot of the boilerplate code for us. We should now have the following files in our project: `UsersFragment`, `UsersRecyclerViewAdapter`, `fragment_users.xml`, and `fragment_users_list.xml`.

We can get rid of a lot of the unnecessary code in our `UsersFragment` and use our own dummy data by replacing it with this:

```kotlin
class UsersFragment : Fragment() {

    private val users = listOf<User>(
        User(1, "Kyle", 29),
        User(2, "Adri", 36),
        User(3, "Andy", 14),
        User(4, "Xayxay", 3),
        User(5, "Mya", 1)
    )
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_users_list, container, false)

        if (view is RecyclerView) {
            with(view) {
                layoutManager = LinearLayoutManager(context)
                adapter = UsersRecyclerViewAdapter(users)
            }
        }
        return view
    }
}
```

Since `UsersRecyclerViewAdapter` is expecting the dummy object that was autogenerated, we will get an error for trying to pass in a list of `User` objects. Let's fix that.

```kotlin
class UsersRecyclerViewAdapter(
    private val users: List<User>
) : RecyclerView.Adapter<UsersRecyclerViewAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.fragment_users, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val user = users[position]
        holder.idView.text = user.id.toString()
        holder.contentView.text = user.name
    }

    override fun getItemCount(): Int = users.size

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val idView: TextView = view.findViewById(R.id.item_number)
        val contentView: TextView = view.findViewById(R.id.content)

        override fun toString(): String {
            return super.toString() + " '" + contentView.text + "'"
        }
    }
}
```

The only changes here were changing the `values` property to `users` and the type to `User` as well. We also made sure the we are binding relevant data to the `ViewHolder` in the `onBindViewHolder` method.

Now that our `UsersFragment` is properly configured, let's setup a new `UserDetailsFragment` (`Right click project folder > New > Fragment > Fragment (Blank)`)

```kotlin
class UserDetailsFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_user_details, container, false)
        
        return view
    }
}
```

We'll also update our `fragment_user_details.xml` to have the pre-generated `TextView` to be in the center of the screen.

```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".UserDetailsFragment">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_blank_fragment"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

Now we have both fragments laid out, we are ready to use them in a navigation graph.

Create a new resource (`Right click res folder > New > Android Resource File`), and create a new Navigation resource called `nav_graph`

![Nav Graph Configuration](https://www.kiloloco.com/images/passing-data-with-safe-args-nav-graph-configuration.png)

Our Editor should now look like this:

![Empty Nav Graph](https://www.kiloloco.com/images/passing-data-with-safe-args-empty-nav-graph.png)

Let's add our fragments to the graph by clicking the (+) icon. 

![Fragments Added to Nav Graph](https://www.kiloloco.com/images/passing-data-with-safe-args-nav-graph-fragments.png)

I added the `usersFragment` first, so it has a house icon next to the name indicating that it will be the entry point for our `nav_graph`. If you added them in a different order, you can simply select the `usersFragment` and tap the house icon at the top to make it the start destination.

I also dragged the navigation action arrow to the `userDetailsFragment` to indicate that we should be able to navigate from `usersFragment` to `userDetailsFragment`.

This is a great visual representation of our app's flow, but if we tried to run the app now, we actually wouldn't see our `usersFragment`. We still need to update our `activity_main.xml` to become our `NavHostFragment`.

Let's replace the pre-generated `TextView` in `activity_main.xml` with a `NavHostFragment`.

```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
    
</androidx.constraintlayout.widget.ConstraintLayout>
```

> **Note**
> You might receive a warning here to use `FragmentContainerView` instead, but the Android documentation only specifies `fragment` at this stage, so we will continue to use it here.

We can head back over to our `nav_graph.xml` and we should see our `activity_main` under the **HOST** section of our **Destinations** panel. 

![Main Activity as NavHostFragment](https://www.kiloloco.com/images/passing-data-with-safe-args-navhostfragment.png)

Now that our `nav_graph` is laid out, we can specify that we want to pass a `User` object from `usersFragment` to `userDetailsFragment`.

In the code of `nav_graph.xml`, I can now add an `argument` within the `userDetailsFragment` attributes:

```xml
... // other userDetailsFragment attributes

<argument
    android:name="user"
    app:argType="com.kiloloco.passing_data.User" />
    
... // </fragment> of userDetailsFragment
```

Here we are specifying that we want our Safe Args to have an argument named `user` which will be of type `User`.

> **Note**
> If you would like to see the different types of arguments you can pass to Safe Args, check out the <a href="https://developer.android.com/guide/navigation/navigation-pass-data" target="_blank">Android Docs</a>.

Now if we build our project, we should have `UsersFragmentDirections` and `UserDetailsFragmentArgs` generated for us.

![Generated files](https://www.kiloloco.com/images/passing-data-with-safe-args-generated-files.png)

These generated objects will help us use type safety when passing our arguments between fragments as well as remove some of the boilerplate required to access the value of the passed argument.

We need to update `UsersRecyclerViewAdapter` and `UsersRecyclerViewAdapter.ViewHolder` to handle passing and receiving the selected `User`.

```kotlin
... // override fun getItemCount(): Int = users.size

inner class ViewHolder(view: View, var user: User? = null) : RecyclerView.ViewHolder(view) {

... // val idView: TextView = view.findViewById(R.id.item_number)
```

```kotlin
... // holder.contentView.text = user.name

holder.user = user

... // onBindViewHolder closing }
```

We added a nullable `user` property to our `ViewHolder`, then updated the `user` property of the `holder` in our `onBindViewHolder` method.

Let's also update our `ViewHolder` to actually handle the selection of the user and the navigation to the `UserDetailsFragment`.

```kotlin
... // val contentView: TextView = view.findViewById(R.id.content)

init {
    view.setOnClickListener {
        user?.let { user ->
            val directions = UsersFragmentDirections.actionUsersFragmentToUserDetailsFragment(user)
            view.findNavController().navigate(directions)
        }
    }
}

... // override fun toString(): String {
```

In our `onClickListenter`, we are passing the `user` as an argument in `UsersFragmentDirections.actionUsersFragmentToUserDetailsFragment` which is expecting a `User` object; that's the sexy type safety we were after 🙌🏽

The `directions` contain the action that is generated from our `nav_graph` which is associated with the flow we created from the `UsersFragment` to the `UserDetailsFragment`. It also passes our `User` object into the bundle which we will be able to access from our `UserDetailsFragmentArgs`.

We can now head over to the `UserDetailsFragment` to handle the receiving of the selected `User`.

```kotlin
... // class UserDetailsFragment : Fragment() {

private val args: UserDetailsFragmentArgs by navArgs()
private lateinit var user: User

... // 
```

We can access our arguments by delegating to `navArgs()` and will need a `lateinit var` that will hang onto the `User`.

```kotlin
... // val view = inflater.inflate(R.layout.fragment_user_details, container, false)

user = args.user

... // return view
```

Since `args` is of type `NavArgsLazy<UserDetailsFragmentArgs>`, we are able to access the `user` property directly from the arguments and store it to `this.user`.

```kotlin
... // onCreateView closing }

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    textView.text = user.description
}

... // UserDetailsFragment closing }
```

With `this.user` updated in `onCreateView`, we can access our `NonNull` `user` to update our UI in `onViewCreated`.

Let's give our app a run, and see if it works as expect 🤞🏽

![App Demo](https://www.kiloloco.com/images/passing-data-with-safe-args-demo.gif)

Now that's a pretty clean way to pass some data! ✨