Jetpack Compose state change not reflected across activities - android

I have an app with two activities. Activity1 changes a State stored in a ViewModel, then starts Activity2. But somehow, the state change is only reflected in Activity1, not in Activity2.
ViewModel
class MyViewModel : ViewModel() {
var hasChanged: Boolean by mutableStateOf(false)
}
Composable of Activity1
#Composable
fun Screen1() {
val context = LocalContext.current
val viewModel: MyViewModel = viewModel()
Column {
Text("State changed: " + viewModel.hasChanged.toString())
Button(
onClick = {
viewModel.hasChanged = true
startActivity(context, Intent(context, Activity2::class.java), null)
}
) {
Text("Change State!")
}
}
}
Composable of Activity2
#Composable
fun Screen2() {
val viewModel: MyViewModel = viewModel()
Text("State changed: " + viewModel.hasChanged.toString()) # stays 'false'
}
Behavior of the app
Activity1 correctly shows the state to be false, initially
After button is pressed, Activity1 correctly displays the state to be true
Activity2 opens but still shows the state to be false
Question
Why is the state change not reflected in Activity2 and can this be fixed?

ViewModels are unique to classes that implement ViewModelStoreOwner
#Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
viewModelStoreOwner.defaultViewModelCreationExtras
} else {
CreationExtras.Empty
}
ComponentActivity implements ViewModelStoreOwner, so it's a ViewModelStoreOwner that contains its own ViewModel instances and uses a HashMap to get same ViewModel for key provided as String.
val vm: MyViewModel = ViewModelProvider(owner = this)[MyViewModel::class.java]
This is how they are created under the hood. This here is Activity or Fragment depending on where ViewModelProvider's owner param is set at. In Compose ViewModelStore owner is accessed by LocalViewModelStoreOwner.current
You need to pass your hasChanged between Activities using Bundle, or have a Singleton repository or UseCase class that you can inject to both ViewModels or use local db or another methods to pass data between Activities.

Related

How to inject ViewModel into Activity using Hilt?

I have a ViewModel that I'm already injecting into a Composable. Now I want to inject the same instance of that ViewModel into my Activity. For example:
In AccountScreen.kt
#Composable
fun AccountScreen(accountViewModel: AccountViewModel = hiltViewModel()) {
...
}
and my Activity class:
class MainActivity : ComponentActivity() {
#Inject
lateinit var accountViewModel: AccountViewModel
}
should have the same instance of AccountViewModel.
I know using #Inject in the Activity as in the example above doesn't work. Hilt's documentation suggests using ViewModelProvider or by viewModels() instead, both of which give me a new instance of AccountViewModel, but I need the same instance as what's in the AccountScreen Composable.
I'm assuming AccountScreen is part of a NavGraph, since you mentioned you need same instance of the view model, you can consider specifying the ViewModelStoreOwner when you inject your ViewModel in your AccountScreen, so MainActivity and AccountScreen will share same instance of it.
#Composable
fun MyNavHost(
...
) {
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(<Destination>) {
AccountScreen(accountViewModel: AccountViewModel = hiltViewModel(viewModelStoreOwner)) {
...
}
}
...
}
}
I ended up solving this by getting the parent Activity's ViewModel in my child Composable (AccountScreen in this case) like so:
val composeView = LocalView.current
val activityViewModel = composeView.findViewTreeViewModelStoreOwner()?.let {
hiltViewModel<MyViewModel>(it)
}
Within my MainActivity I'm getting the ViewModel the standard way
private val accountViewModel: AccountViewModel by viewModels()
Thanks to #z.g.y for providing a helpful suggestion that led me to this solution.

Multiple Instances of ViewModel in Hilt

I apologize if this has been asked before. I am trying to create multiple instances of the same type of viewmodel scoped to an activity using dagger-hilt, but even with different custom default args, it is returning the same instance each time.
I need all the viewmodel instances to be activity scoped, not fragment or navgraph scoped because I need all the fragments to subscribe to the updated data that will be received in the activity.
(Using Kotlin)
Activity Code
#AndroidEntryPoint
class Activity : AppCompatActivity() {
private val vm1:MyViewModel by viewModels(extrasProducer = {
val bundle = Bundle().apply {
putString("ViewModelType", "vm1")
}
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(DEFAULT_ARGS_KEY, bundle)
}
}) {
MyViewModel.Factory
}
private val vm2:MyViewModel by viewModels(extrasProducer = {
val bundle = Bundle().apply {
putString("ViewModelType", "vm2")
}
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(DEFAULT_ARGS_KEY, bundle)
}
}) {
MyViewModel.Factory
}
...
}
ViewModel Code
#HiltViewModel
class MyViewModel #Inject constructor(
application: Application,
private val myRepo: MyRepository,
private val savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
...
// Define ViewModel factory in a companion object
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
val defaultArgs = extras[DEFAULT_ARGS_KEY]
println("extras $extras and default $defaultArgs")
// Get the Application object from extras
val application = checkNotNull(extras[APPLICATION_KEY])
// Create a SavedStateHandle for this ViewModel from extras
val savedStateHandle = extras.createSavedStateHandle()
savedStateHandle.keys().forEach {
println("factory $it, ${savedStateHandle.get<Any>(it)}")
}
return MyViewModel(
application = application,
myRepo = MyRepository(application),
savedStateHandle = savedStateHandle
) as T
}
}
}
}
When I print out the default arguments, the first initialized viewmodel is always returned, and is not initialized again even with both variables in the activity having different default arguments. Expected result: New viewmodel instance with different default arguments.
I think it has to do with the Viewmodel store owner key being the same, but I do want the viewmodel store owner to be the same, just as a new instance, if that makes sense.
I know that in the past you could use AbstractSavedStateViewModelFactory, or a custom viewmodel factory with ViewModelProvider.get(), but I can't access ViewModelProvider.get without passing a ViewModelStoreOwner, and since I don't want to pass it to the factory since it could leak the activity, I'm confused as to how to go about this. Is there a better way than using hilt to create multiple instances of the same type of viewmodel in the same scope?
override val viewModel: MyViewModel by activityViewModels()
Create instance of viewModel which lives with activity.

How can I have different states with different viewmodels?

I am making an app where the user first need to login to be able to get alot of different data from a backend. (many endpoints)
So I have one viewmodel for the login, and I have alot of viewmodels for all the other data.
The other viewmodels require the token from the first viewmodel to be able to get data from the backend.
I don't know how I can do this.
I was thinking that I can have my login screen in a kind of state manager which will direct the UI to the correct screen like this
#ExperimentalComposeUiApi
#Composable
fun LoginState(vm: AuthViewModel, nc: NavController) {
val token by vm.token.collectAsState()
when (token) {
is Resource.Loading -> {
LoadingScreen()
}
is Resource.Success -> {
Scaffold(vm = vm)
}
is Resource.Error -> {
LoginScreen(vm = vm)
}
}
}
But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
Another thought was to use Hilt to do some kind of magic dependency injection, and then put all the viewmodels into a ViewModelManager in the MainActivity and then inject the Token into the repositories of each viewmodel when login is successfull.
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val authViewModel: AuthViewModel by viewModels()
private val userViewModel: UserViewModel by viewModels()
private val venueViewModel: VenueViewModel by viewModels()
private val eventViewModel: EventViewModel by viewModels()
private val viewModelManager = ViewModelManager(
userViewModel = userViewModel,
authViewModel = authViewModel,
venueViewModel = venueViewModel,
eventViewModel = eventViewModel,
)
#ExperimentalMaterialApi
#ExperimentalComposeUiApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MoroAdminTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
ScaffoldExample(viewModelManager)
}
}
}
}
}
However I have no idea how to do this or if it is even possible - or a good solution.
Problem: you want to share a value (token) to all of your view model
your token retrieved in AuthViewModel and need to share it to the other viewModels
you can make your data in the other viewModels changes when the token changes
by using datastore Preferences see implementation
Datastore preferences provides you with a flow of values whenever the value changes
Create a DatastoreManager Class
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
#Singleton
class DatastoreManager #Inject constructor(#ApplicationContext context: Context) {
private val dataStore = context.dataStore
val dataFlow = dataStore.data
.map { preferences ->
val token = preferences[PreferencesKeys.TOKEN]
}
suspend fun updateToken(token: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.TOKEN] = token
}
}
private object PreferencesKeys {
val TOKEN = preferencesKey<String>("token")
}
}
In AuthViewModel
Inject the DatastoreManager and set the token after login
datastore.updateToken(newToken)
In other ViewModels
Inject the DatastoreManager and use it's value
//this is a flow of tokens and will recive the token when you set it
val token = datastore.token
// if you are not familiar with flows and using only LiveData
val token = datastore.token.asLiveData()
// use the token to get the data from backend
val data = token.map {
// this code will trigger every time the token changes
yourGetDataFromBackendFunction(it)
}
But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
This is not true. You don't have to create view models in your Activity.
In any composable you can use viewModel()
Returns an existing ViewModel or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided by LocalViewModelStoreOwner.
So you don't need any ViewModelManager. Inside any composable you can use viewModel() with the corresponding class. In your case you're using Hilt, you should use hiltViewModel() instead: it'll also initialize your injections.
#Composable
fun AuthScreen(viewModel: AuthViewModel = hiltViewModel()) {
}
Or like this:
#Composable
fun VenueScreen() {
val viewModel: VenueViewModel = hiltViewModel()
}
First approach will allow you to easily test your screen with mock view model, without passing any arguments in your production code.
Check out more about view models in view models documentation and hilt documentation
As to your token question, you can pass it with injections. I don't think that your view model really needs the token, probably you should have some network manager which will use the token to make requests. And this network manager should use injection of some token provider.

SharedViewModel between fragment and its host activity by Koin

I want to achieve the communication between fragment and its host activity by using ViewModel(following: Share data using a ViewModel) to update the UI of activity when shared LiveData changed.
Start with declare ViewModel in the module
MainModule.kt
object MainModule {
val module = module {
viewModel {
MainViewModel()
}
}
}
Then inject it to activity and fragment
MainActivity.kt
private val mainViewModel by viewModel<MainViewModel>()
MainFragment.kt
private val mainViewModel by sharedViewModel<MainViewModel>()
Observe the change of LiveData on activity
MainActivity.kt
mainViewModel.drawerState.observe(this, {
// do something when it changed
})
Update the LiveData when the button(on fragment) clicked
MainFragment.kt
mainButton.setOnClickListener {
mainViewModel.toggleDrawerState()
}
The LiveData declare in ViewModel
MainViewModel.kt
private val _drawerState = MutableLiveData<DrawerState>()
val drawerState: LiveData<DrawerState> = _drawerState
fun toggleDrawerState() {
if (_drawerState.value == DrawerState.OPENED) {
_drawerState.value = DrawerState.CLOSED
} else {
_drawerState.value = DrawerState.OPENED
}
}
DrawerState.kt
enum class DrawerState {
CLOSED, OPENED
}
But It does not work as expected which means nothing happens when the button clicked(can guarantee by debugging with breakpoint). I wondering to know where I've gone wrong or misunderstood. Thank you.

Pass arguments from fragment to viewmodel function

Can you tell me if my approach is right? It works but I don't know if it's correct architecture. I read somewhere that we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request but I really need to pass arguments from one viewmodel to another one. Important thing is I'm using Dagger Hilt dependency injection so creating factory for each viewmodel isn't reasonable?
Assume I have RecyclerView of items and on click I want to launch new fragment with details - common thing. Because logic of these screens is complicated I decided to separate single viewmodel to two - one for list fragment, one for details fragment.
ItemsFragment has listener and launches details fragment using following code:
fun onItemSelected(item: Item) {
val args = Bundle().apply {
putInt(KEY_ITEM_ID, item.id)
}
findNavController().navigate(R.id.action_listFragment_to_detailsFragment, args)
}
Then in ItemDetailsFragment class in onViewCreated function I receive passed argument, saves it in ItemDetailsViewModel itemId variable and then launch requestItemDetails() function to make api call which result is saved to LiveData which is observed by ItemDetailsFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
val itemId = arguments?.getInt(KEY_ITEM_ID, -1) ?: -1
viewModel.itemId = itemId
viewModel.requestItemDetails()
//...
}
ItemDetailsViewModel
class ItemDetailsViewModel #ViewModelInject constructor(val repository: Repository) : ViewModel() {
var itemId: Int = -1
private val _item = MutableLiveData<Item>()
val item: LiveData<Item> = _item
fun requestItemDetails() {
if (itemId == -1) {
// return error state
return
}
viewModelScope.launch {
val response = repository.getItemDetails(itemId)
//...
_item.postValue(response.data)
}
}
}
Good news is that this is what SavedStateHandle is for, which automatically receives the arguments as its initial map.
#HiltViewModel
class ItemDetailsViewModel #Inject constructor(
private val repository: Repository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val itemId = savedStateHandle.getLiveData(KEY_ITEM_ID)
val item: LiveData<Item> = itemId.switchMap { itemId ->
liveData(viewModelScope.coroutineContext) {
emit(repository.getItemDetails(itemId).data)
}
}
we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request
Yes, in your example a request will be executed whenever ItemDetailsFragment's view is created.
Take a look at this GitHub issue about assisted injection support in Hilt. The point of assisted injection is to pass additional dependencies at object's creation time.
This will allow you to pass itemId through the constructor, which then will allow you to access it in ViewModel's init block.
class ItemDetailsViewModel #HiltViewModel constructor(
private val repository: Repository,
#Assisted private val itemId: Int
) : ViewModel() {
init {
requestItemDetails()
}
private fun requestItemDetails() {
// Do stuff with itemId.
}
}
This way the network request will be executed just once when ItemDetailsViewModel is created.
By the time the feature is available you can either try workarounds suggested in the GitHub issue or simulate the init block with a flag:
class ItemDetailsViewModel #ViewModelInject constructor(
private val repository: Repository
) : ViewModel() {
private var isInitialized = false
fun initialize(itemId: Int) {
if (isInitialized) return
isInitialized = true
requestItemDetails(itemId)
}
private fun requestItemDetails(itemId: Int) {
// Do stuff with itemId.
}
}

Categories

Resources