'How to communicate betweeen viewmodels in jetpack compose

I have a home screen in my application that is basically content with a navigation bar

Each of the three selections of the navigation bar lead to a different screen, so the code looks like this:

@Composable
fun HomeScreen(state: HomeState, event: (HomeEvent) -> Unit) {
    val navController = rememberNavController()
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            BottomNavigation { .... //add the three bottom navigation menu items
            }
        },
    ) {
        NavHost(
            navController = navController,
            startDestination = "news",
        ) {
            composable(route = "news") {
                val newsVm: NewsViewModel = hiltViewModel()
                NewsScreen(newsVm)
            }
            composable(route = "tickets") { NewTicketScreen() } 
            composable(route = "archive") { ArchiveScreen() }
        }
    }
}

this works correctly

this homescreen is used by the following composeable to actually draw the screen

@Composable
fun HomeScreen(
    vm: HomeViewModel = hiltViewModel()
) {
    val state = vm.state.value
    HomeScreen(state, vm::process )
}

so HomeScreen has its own viewmodel

in this example let us take the NewsScreen which takes as an argument its own viewmodel

What this viewmodel will do is load news articles and show them to the user. But in order to not have to reload data every time the user changes the shown screen, what I would do before compose, is pass the homeViewModel as an argument to the newsViewModel.

Home would contain the data loaded up to now and expose it to its children.

and news would load data and save the loaded data in homeViewmodel

so it would go something like this

class HomeViewModel()..... { internal val newsArticles = mutableListOf() }

class NewsViewModel() ..... {
    val parent :HomeViewModel = ????
    val list = mutableStateOf<List<NewsArticle>>(listOf())
    init {
        val loaded = parent.newsArticles
        loadData(loaded) 
    }

    fun loadData(loaded :List<NewsArticle>) {
       if (loaded.isEmpty()) {
           list.value = repo.loadNews()
       } else {
           list.value = loaded
       }
    }
}

I know that I could do the above in my repository, and have it do the caching, but I also use the homeViewModel for communication between the screens , and if the user has to log in , the app uses the MainActivity's navController to start a new screen where the user will log in.

Is there a way to have a reference to the parent viewmodel from one of the children?



Solution 1:[1]

You can either explicitly call the viewmodel that you want to contact by injecting both viewmodel belonging to same nav graph.

Alternatively, you can share a interface among both viewmodels, ensure it is same instance and use it as communication bridge.

interface ViewModelsComBridge<T>{
  fun registerCallback(onMessageReceived : (T) -> Unit)
  fun onDispatchMessage(message : T)
  fun unregister(onMessageReceived : (T) -> Unit)
}

and in your view models:

class ViewModelA @Inject constructor(private val bridge : ViewModelCommunicationBridge<MyData>, ...) : ViewModel(){

 init {
    bridge.registerCallback { //TODO something with call }  
  }
}

in second view model:

class ViewModelA @Inject constructor(private val bridge : ViewModelCommunicationBridge<MyData>, ...) : ViewModel(){

  fun onClick(){
  val myData = processMyData()
  bridge.onDispatchMessage(myData)
 }

}

On the other end the other viewmodel will receive this call if it is alive.

Ensure your implementation is inject correctly and it is same instance in both viewmodels.

Solution 2:[2]

Your can change your NewsViewModel 's viewModelStoreOwner(fragment, activity or HomeScreen's destination), not the lifecycle of news's destination. so your data will be survive while NewsScreen changes.

@Composable
fun HomeScreen(state: HomeState, event: (HomeEvent) -> Unit) {
    val navController = rememberNavController()
    val newsVm: NewsViewModel = hiltViewModel() //move to here, 
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            BottomNavigation { .... //add the three bottom navigation menu items
            }
        },
    ) {
        NavHost(
            navController = navController,
            startDestination = "news",
        ) {
            composable(route = "news") {
                NewsScreen(newsVm)
            }
            composable(route = "tickets") { NewTicketScreen() } 
            composable(route = "archive") { ArchiveScreen() }
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Nikola Despotoski
Solution 2 CTD