Hi there! Welcome to My Blog.

I’m Raj, my goal is to show you how you can use SavedStateHandle to receive arguments in ViewModel Compose Android. I hope you will find this content useful.

You can take reference of the source code from GitHub repository.

1. Composable destinations

Now we have two screens as composable destinations in the navigation graph.
A first composable screen which shows list of movie items.

composable(
    route = "list_route"
) {
    ListScreen(
        selectedMovie = { movieId: Int ->
            navController.navigate("detail_route/$movieId")
        }
    )
}

A second composable screen which shows details of the selected movie from first screen with movieId.

composable(
    route = "detail_route/movieId",
    arguments = listOf(
        navArgument("movieId") {
            type = NavType.IntType
        }
    )
) {
    val viewModel = hiltViewModel<DetailViewModel>()
    DetailScreen(
        viewModel = viewModel
    )
}

We want to send id of the clicked movie item from first composable screen list_route to other composable destination detail_route.

2. SavedStateHandle in ViewModel

SavedStateHandle is key-value map that will let you retrieve value from the saved state with key.

So from the routes that we use to navigate, we are passing the key name movieId which will be set with a value while navigating from list to detail screen.

We will use the same key movieId to retrieve the value saved in it inside the DetailViewModel.
Add SavedStateHanlde directly as a constructor argument to the ViewModel.

class DetailViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val movieId: Int? = savedStateHandle["movieId"]

    fun getMovieInformation() {
        val movieData: Movie = repository.getSelectedMovieData(movieId)
    }
}

In Hilt injected ViewModels, we can inject same way SavedStateHandle into constructor.

@HiltViewModel
class DetailViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val movieId: Int? = savedStateHandle["movieId"]
    ...
}

By default, the values we retrieve from SavedStateHandle will be of nullable type.
Wrap the handle key with checkNotNull or !! to receive not-null type.

private val movieId: Int = checkNotNull(savedStateHandle["movieId"])

3. Supported observable types

We can retrieve above values as observable types too. This might not be useful while we have navigated with information or ID which won’t change.

But if we have a scenario where changes need to happen in same screen we can update the value from the SavedStateHandle with Key, or when user submits a news search query or update the data from the TextField value changes.

Retrieve from LiveData :

class SearchViewModel(
    private val savedStateHandle: SavedStateHandle,
    private val searchRepository: SearchRepository
) : ViewModel() {

    val movieData: LiveData<List<Movie>> =
        savedStateHandle.getLiveData<String>("search_query").switchMap { query ->
            searchRepository.getFilteredData(query)
        }

    fun setQuery(searchQuery: String) {
        savedStateHandle["search_query"] = searchQuery
    }
}

Retrieve from StateFlow:

class SearchViewModel(
    private val savedStateHandle: SavedStateHandle,
    private val searchRepository: SearchRepository
) : ViewModel() {

    val movieData: StateFlow<List<Movie>> =
        savedStateHandle.getStateFlow<String>("search_query").flatMapLatest { query ->
            searchRepository.getFilteredData(query)
        }

    fun setQuery(searchQuery: String) {
        savedStateHandle["search_query"] = searchQuery
    }
}

4. NavType using Parcelable types

Annotate the class with Parcelize type and implement Parcelable type.

import kotlinx.parcelize.Parcelize

@Parcelize
data class Movie(
    val movieId: Int
): Parcelable

In receiver, change the NavType to ParcelableType and pass the class information.

composable(
    route = "detail_route/movie",
    arguments = listOf(
        navArgument("movie") {
            type = NavType.ParcelableType(Movie::class.java)
        }
    )
) {
    val viewModel = hiltViewModel<DetailViewModel>()
    DetailScreen(
        viewModel = viewModel
    )
}

Finally retrieve the parcelable class with it’s type with SavedStateHandle.

@HiltViewModel
class DetailViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val movie: Movie? = savedStateHandle["movie"]
}

5. Testing SavedStateHandle

We can test the SavedStateHandle by simply mocking the information we are receiving and pass that value to the DetailViewModel.

class DetailViewModelTest {
    
    private val savedStateHandle = mockk<SavedStateHandle>(relaxed = true)
    private val movie = mockk<Movie>()
    
    private lateinit var viewModel: DetailViewModel

    @BeforeEach
    fun setUp() {
        every { savedStateHandle.get<Movie>("movie") } returns movie
    }

    @Test
    fun `savedStateHandle should fetch movie information`() {
        every { savedStateHandle["movie"] = movie }

        viewModel = DetailViewModel(
            savedStateHandle = savedStateHandle
        )
        
        // Perform your assertion logic
        expectThat(true).isEqualTo(true)
    }
}

That’s it!

Take a look at GitHub repository which I have linked below which has complete code example with more destinations.

Read all the detailed guidelines from Android documentation, it covers everything in much depth.

Project code and resources :

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.