I've a ViewModel (say `ProfileViewModel`) that has...
# compose
r
I've a ViewModel (say
ProfileViewModel
) that has a function that fetch posts (paging data) of given user. I've 2 composables namely
ProfileView
and
DetailedPostView
I need to use same function to load posts with cached value in viewmodel, but when I navigate to detailed screen, it re-fetch the data instead of cached value. How to avoid it? code is in thread.
Copy code
class ProfileViewModel @Inject constructor(private val profileRepository: ProfileRepository) : ViewModel() {
    fun getUserPosts(userId: Long): Flow<PagingData<Post>> =
        profileRepository
            .getUserPosts(userId)
            .cachedIn(viewModelScope)
}

@Composable
fun ProfileScreen(
    navController: NavController,
    userId: Long,
    profileViewModel: ProfileViewModel,
) {
    val posts = remember(profileViewModel) { profileViewModel.getUserPosts(userId) }.collectAsLazyPagingItems()
    // Navigate to PostDetailsScreen
}

@Composable
fun PostDetailsScreen(
    navController: NavController,
    userId: Long,
    profileViewModel: ProfileViewModel
) {
    val posts = remember(profileViewModel) { profileViewModel.getUserPosts(userId) }.collectAsLazyPagingItems()
}

@Composable
fun Navigation(navController: NavHostController) {
    composable(
        route = Screens.PostDetails.route + "/{userId}",
        arguments = listOf(navArgument(name = "userId") { type = NavType.LongType })
    ) { backStackEntry ->
        val profileViewModel = hiltViewModel<ProfileViewModel>()
        PostDetailsScreen(
            navController = navController,
            userId = backStackEntry.arguments?.getLong("userId"),
            profileViewModel = profileViewModel
        )
    }
}
I suspect below is the reason for this. If it is, how to avoid it?
Copy code
Regardless of whether you use viewModel() or with Hilt hiltViewModel() to retrieve your ViewModel, they both will call onCleared() when the NavHost finishes transitioning to a different route
a
I had created a scoped view model extension function. Not sure how to implement it in DI world or only compose world. You will provide a destination to the scopedViewModel function. This will use the same viewModel instance if provided destination is in the backstack. I.e., it will use provided destination's viewModelStore to create (or get instance) of the viewModel if it's in the backstack.
Copy code
@MainThread
inline fun <reified VM : ViewModel> Fragment.scopedViewModel(
        @IdRes navId: Int,
        noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
    val backStackEntry by lazy {
        try {
            findNavController().getBackStackEntry(navId)
        } catch (e: Exception) {
            this
        }
    }
    val storeProducer: () -> ViewModelStore = {
        backStackEntry.viewModelStore
    }
    return createViewModelLazy(VM::class, storeProducer, {
        factoryProducer?.invoke() ?: backStackEntry.defaultViewModelProviderFactory
    })
}
r
Thanks for this. But I'm looking for built in compose only solution if available.
s
If you’re using hilt and compose navigation you can get a ViewModel scoped to a parent navigation route therefore sharing the same instance of it https://developer.android.com/jetpack/compose/libraries#hilt-navigation. If not I’m pretty sure there are other people on this slack that have asked this question. Look for answers by Ian Lake, he usually answers this kind of stuff.
👍 1
r
@Stylianos Gakis tried below code, but its still re-fetching the data
Copy code
navigation(
    startDestination = Screens.Home.route,
    route = Screens.UserProfile.route
) {
    composable(
        route = Screens.PostList.route + "/{userId}/{index}",
        arguments = listOf(
            navArgument(name = "userId") {
                type = NavType.LongType
            },
            navArgument(name = "index") {
                type = NavType.IntType
            }
        )
    ) { backStackEntry ->
        val parentEntry =
            remember { navController.getBackStackEntry(Screens.UserProfile.route) }
        val userProfileVM = hiltViewModel<UserProfileViewModel>(parentEntry)
        PostListScreen(
            navController = navController,
            userId = backStackEntry.arguments?.getLong("userId"),
            index = backStackEntry.arguments?.getInt("index"),
            homeViewModel = homeViewModel,
            userProfileViewModel = userProfileVM
        )
    }
}
s
Well you are calling
getUserPosts
on both compostables, doing so on the same VM instance doesn't change that fact. I'm not familiar with the paging library, but there should be a way to just observe the incoming data instead of also triggering the fetching of them
Either a shared VM, or a repository/cache abstraction that sits outside of your UI/VM layer.