https://kotlinlang.org logo
Title
r

Robert Menke

01/28/2022, 9:41 PM
Hey, I have an architecture question. I have a standard compose app with navigation-compose and an
AuthenticationViewModel
which is a
@HiltViewModel
that keeps track of the authenticated
User
entity as follows:
private var authenticatedUser: LiveData<User?> = userRepository.observeAuthenticatedUser().asLiveData()
After I’ve verified that a user is authenticated I want to be able to observe
authenticatedUser
and ideally receive the latest`User` instance without having to retrieve it from the database again. I have a component called
ProfileScreen
that tries to access that data as follows:
val authViewModel: AuthenticationViewModel = hiltViewModel()
val user by authViewModel.getAuthenticatedUser().observeAsState()
val firstName by viewModel.firstName.observeAsState(user?.firstName ?: "")
When I set a breakpoint on
firstName
the variable
user
is always
null
initially. What is the right way to keep my
User
instance in memory without having to re-fetch from the DB whenever I ask for it?
i

Ian Lake

01/28/2022, 11:29 PM
If you look at the source code for `asLiveData()`, you'll see that it calls
collect
on the
Flow
, which is inherently an async operation. Now if your
UserRepository
was driven by Room and returned a
LiveData
itself, then you'd gain the natural caching that
LiveData
has where it always remembers any previously returned value just like how
StateFlow
works
Personally, I'd probably skip using LiveData at all here and use something like:
private var authenticatedUser: StateFlow<User?> = userRepository.observeAuthenticatedUser().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
Which would give you a
StateFlow
(which also always remembers its current value), that is Lifecycle aware (the
WhileSubscribed
prevents it from doing any work while your UI isn't collecting the StateFlow), and that still lets your Composable code call
collectAsState()
to easily gather the latest value
r

Robert Menke

01/28/2022, 11:58 PM
Thank you @Ian Lake I’ll give that a try tonight. This is a follow up from the same question I asked you on twitter and once again I want to express gratitude for your time and expertise. I’ll reply in thread with my solution if/when I get there.
👍 1
Alright I think I finally understand my main issue after setting some breakpoints within
@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
    viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is NavBackStackEntry) {
    HiltViewModelFactory(
        context = LocalContext.current,
        navBackStackEntry = viewModelStoreOwner
    )
} else {
    // Use the default factory provided by the ViewModelStoreOwner
    // and assume it is an @AndroidEntryPoint annotated fragment or activity
    null
}
I had a
NavBackStackEntry
for my
SplashScreen
and a separate one for my
ProfileScreen
and yet another that didn’t have any associated
NavBackStackEntry
, so of course I was re-fetching user state after the initial authentication check. Seems like the right solution for something like authenticated user state is to create 1 instance of the view model outside of the navigation graph and then pass that instance into each
Composable
that needs it since being scoped to
@AndroidEntryPoint
I believe makes more sense here. I think I’ll move forward with that for now.
Yep, that did the trick 👍