sebaslogen / resaca

Android library to scope ViewModels to a Composable, surviving configuration changes and navigation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ViewModelScope not separated

vladraduvidican opened this issue · comments

Hello!

I have two viewmodels of the same kind, both listening to inputs from a Firebase snapshotlistener (similar to a websocket).

The first viewmodel is running. I navigate to another screen with the same type of viewmodel running (so the first screen with the first viewmodel is not visible, it is in the backstack). I initialise the second viewmodel to listen to that websocket I was talking about, and it seems that I also receive data from the first viewmodel, updating the uistate in my second viewmodel.

Hi, what I understand is that your two ViewModels of the same type (but different instances) are getting the same dependency with the same state/data (Firebase listener), did I get it right?

To be able to help you, the best way would be a sample app/code. You could also help me understand better what is exactly you expected to happen, plus some other info to help clarify the issue:

  • What do you use for navigation between these two screens? (Compose-navigation/Fragments/Activities/a 3rd party lib)
  • Were the viewModels retrieved with viewModelScoped or hiltViewModelScoped?
  • Do you use Hilt or Koin? If so, can you share the constructors of your ViewModel and the Firebase snapshotlistener with any annotations that you use to scope and inject them?

That's correct. I am using compose navigation, hiltViewModelScoped with separate keys and Hilt.

I will get back with an example in about a week I apologise I'm not back until then.

Using Hilt and hiltViewModelScoped there are several annotations available to use in case you want the same object or new instances injected per ViewModel (e.g. @ViewModelScoped for separate instances or @ActivityRetainedScoped for shared instance). This article from Google might help you: https://medium.com/androiddevelopers/using-hilts-viewmodelcomponent-53b46515c4f4

Note: The key in hiltViewModelScoped is not really needed to have 2 VMs, this key is useful only when you want to re-create a VM when some key has changed in the current screen.

Hello!
Thank you for your response! I have a UiState initiated in the ViewModel, as such:

data class LogViewUiState(
    val loading: Boolean = true,
    val allLogItems: List<Log>? = null,
)

@HiltViewModel
class LogViewViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val adminRepository: AdminRepository,
) : ViewModel() {
    private val _uiState = MutableStateFlow(LogViewUiState())
    val uiState: StateFlow<LogViewUiState> = _uiState
}

The UiState is initialised inside the ViewModel, doesn't that already make it scoped to that viewmodel?

Adding @ViewModelScoped and putting the state in the constructor returns this:
LogViewUiState cannot be provided without an @Inject constructor or an @Provides-annotated method.

The UiState is initialised inside the ViewModel, doesn't that already make it scoped to that viewmodel?

Yes, since the LogViewUiState object is created inside the VM it is scoped to the VM and it is not part of the Hilt/DI setup.

If you obtain this LogViewViewModel VM using val vm: LogViewViewModel = hiltViewModelScoped() on two Composables that are part of two different Compose navigation destinations, then the VMs and their uiState will be different per destination.

You can test this easily by updating only the loading boolean after a hardcoded delay of 5 seconds. Once you open the first destination you should see the loader for 5 seconds and then disappear after the 5 seconds, then if you navigate to the second destination you should see the loader again for 5 seconds on the second destination because the VM and its uiState are different.

Adding @ViewModelScoped and putting the state in the constructor returns this: LogViewUiState cannot be provided without an @Inject constructor or an @Provides-annotated method.

This message means that LogViewUiState needs @Inject annotation to be part of the dependency injection graph. But if you don't want to share these uiState objects between VMs, then it's better that you don't inject them and keep the setup you already had: private val _uiState = MutableStateFlow(LogViewUiState())


I have tested your loading use case in the demo app and it works just as I described above. If you want to test it too: open this repository in Android Studio -> Use the code below in the class FakeSecondInjectedViewModel -> Add some breakpoints to see the value of loading boolean -> Run the demo app on a device or emulator:

@HiltViewModel
class FakeSecondInjectedViewModel @Inject constructor(
    private val stateSaver: SavedStateHandle,
    private val viewModelsClearedCounter: AtomicInteger
) : ViewModel() {

    var loading = true

    init {
        viewModelScope.launch {
            delay(30000)
            loading = false
        }
    }
    override fun onCleared() {
        super.onCleared()
        viewModelsClearedCounter.incrementAndGet()
    }
}

Back to your original problem and sample code, you mentioned that the same state appears in both VMs and that they both use Firebase, I suspect the source of the shared state is the data coming from Firebase and used in both VMs

Thank you for your response! It turned out this was a Firebase specific issue which really made it seem like the same state is shared across viewmodels.