Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Sharing data between ViewModels in Compose
Aleksei Laptev
Aleksei Laptev

Posted on • Edited on

     

Sharing data between ViewModels in Compose

This article was inspired bythis post and comments below. I wanna write about some common patterns about sharing data.

for code samples I'm using Hilt and Navigation frameworks


Use shared view model

Description: we use a single ViewModel with several screens.

It looks something tike this:

SharedViewModel

@HiltViewModelclassSharedViewModel:ViewModel(){privateval_someString=MutableStateFlow("")valsomeString=_someString.asStateFlow()}
Enter fullscreen modeExit fullscreen mode

Screen1

@ComposablefunScreen1(viewModel:SharedViewModel=hiltViewModel()){valsomeStringbyviewModel.someString.collectAsState()}
Enter fullscreen modeExit fullscreen mode

Screen2

@ComposablefunScreen1(viewModel:SharedViewModel=hiltViewModel()){valsomeStringbyviewModel.someString.collectAsState()}
Enter fullscreen modeExit fullscreen mode

So, what can I say? I just repeat my own words from comments of started post:

It's not the best approach. This way can be applied only for a small apps. When the app is becoming large, your viewmodel is becoming bigger and bigger. And one moment you can realize that the viewmodel is your data source in fact.

One thing I'd like to add: but generally there are a lot of situations when should use SharedViewModel. For instance, you have two screens that work with the same data (1st - for read, 2nd - for edit), it makes sense to use SharedViewModel in this case.


Navigate with arguments

Description: we can pass a simple data like an argument in navigation route.

It looks something like this:

NavGraph

objectNavDestinations{constvalScreen1="Screen1"constvalScreen2="Screen2"}@ComposablefunNavGraph(viewModel:MainViewModel,onLocaleChange:()->Unit){valnavController=rememberNavController()NavHost(navController,NavDestinations.Screen1){composable(NavDestinations.Screen1){Screen1(navController)}composable("${NavDestinations.Screen2}/{id}"){backStackEntry->valid=backStackEntry.arguments?.getString("id")!!Screen2(navController)}
Enter fullscreen modeExit fullscreen mode

Screen2ViewModel

@HiltViewModelclassScreen2ViewModel@Injectconstructor(savedStateHandle:SavedStateHandle,privatevalgetDataUseCase:GetDataUseCase):ViewModel(){privatevalid=savedStateHandle.get<Int>("id")?:0valdata=getDataUseCase.getDataById(id).stateIn(scope=viewModelScope,started=SharingStarted.WhileSubscribed(5_000),initialValue=Data())}
Enter fullscreen modeExit fullscreen mode

Screen1

@ComposablefunScreen1(navController:NavController,viewModel:Screen1ViewModel=hiltViewModel()){// some codeButton(onClick={navController.navigate(NavDestinations.Screen2+"/${data.id}")}){Text("Click me")}}
Enter fullscreen modeExit fullscreen mode

Screen2

@ComposablefunScreen2(navController:NavController,dataViewModel:Screen2ViewModel=hiltViewModel()){valdatabydataViewModel.data.collectAsState()// some code}
Enter fullscreen modeExit fullscreen mode

More details you may see inthis article, for example.

It's an old approach and not actual now because we've got astable release of Navigation library in Dec 2024.


Navigate with data

Ok, next we can send an object.link to docs

I don't know should I copy code from Google docs or not. Okay, make a copy for collecting in one place.

Serializable object

@Serializabledata classProfile(valname:String)
Enter fullscreen modeExit fullscreen mode

NavDestination

// Pass only the user ID when navigating to a new destination as argumentnavController.navigate(Profile(id="user1234"))
Enter fullscreen modeExit fullscreen mode

ViewModel

classUserViewModel(savedStateHandle:SavedStateHandle,privatevaluserInfoRepository:UserInfoRepository):ViewModel(){privatevalprofile=savedStateHandle.toRoute<Profile>()// Fetch the relevant user information from the data layer,// ie. userInfoRepository, based on the passed userId argumentprivatevaluserInfo:Flow<UserInfo>=userInfoRepository.getUserInfo(profile.id)// …}
Enter fullscreen modeExit fullscreen mode

So, you can send "primitive" types, string, recourse reference, parcelable, serializable end enum types.link to docs

One thing I'd like to add: I don't know how it works right now, but before the stable release it was a real challenge to send complex serializable object. It wasn't worth it.

My advice: if you need to send something, send a small piece of data such as "id" and get information from a normal data-source.


Use repository in data layer

The main concept: Make a repository, inside of repository make a StateFlow with value. So, ViewModels subscribe to flow. Add a function with emit value in the repo. As a result we have a single data source and necessary amount ViewModels that don't depend on each other.

Quote byJihad Mahfouz

In large apps, data shouldn't be kept in memory. For that neither viewModels nor repositories should be used, we can pass a sort of reference like an ID as an argument to the next screen, and then look up the data for it

And you're absolutely right! So special for this approach I post an example from a real project (without NDA).

Okay, we have a large data what is keep in the database. Also we have two screens, each of them has its own ViewModel, obviously. On the 1st screen we have to show some kind of data from database, on the 2nd screen we choose necessary filters (lots of them) that we need to apply to data request.

It was something like this:

FiltersRepositoryImpl

classFiltersRepositoryImpl@Injectconstructor(@DefaultDispatcherprivatevaldefaultDispatcher:CoroutineDispatcher):FiltersRepository{privateval_filterStateFlow=MutableStateFlow(Filters(cond1,cond2))overridefungetFilterStateFlow():StateFlow<Filters>{return_filterStateFlow.asStateFlow()}overridesuspendfunupdateFilter(filters:Filters){withContext(defaultDispatcher){_filterStateFlow.emit(filters)}}}
Enter fullscreen modeExit fullscreen mode

GetDataUseCaseImpl

classGetDataUseCaseImpl@Injectconstructor(privatevaldataRepository:DataRepository,@IoDispatcherprivatevalioDispatcher:CoroutineDispatcher,// etc.):GetDataUseCase{overridefungetDataListFlow(filters:Filters):Flow<List<Data>>{returndataRepository.getFilteredDataFlow(filters).map{dataDbViewList->dataDbViewList.map{dataDbView->mapToModel(dataDbView)}}.flowOn(ioDispatcher)}// some code, mapper etc.}
Enter fullscreen modeExit fullscreen mode

FiltersScreenViewModel

@HiltViewModelclassFiltersScreenViewModel@Injectconstructor(privatevalfiltersRepository:FiltersRepository):ViewModel(){varfilters:FiltersbymutableStateOf(Filters(cond1,cond2))init{viewModelScope.launch{filtersRepository.getFilterStateFlow().collectLatest{filters=it}}}funapplyFilters(){viewModelScope.launch{filtersRepository.updateFilter(filters.copy(someCond))}}// some code}
Enter fullscreen modeExit fullscreen mode

DataScreenViewModel

@OptIn(ExperimentalCoroutinesApi::class)@HiltViewModelclassDataScreenViewModel@Injectconstructor(privatevalgetDataUseCase:GetDataUseCase,privatevalfiltersRepository:FiltersRepository):ViewModel(){valdataListFlow:StateFlow<List<Data>>=filtersRepository.getFilterStateFlow().flatMapLatest{getDataUseCase.getDataListFlow(it)}.stateIn(scope=viewModelScope,started=SharingStarted.WhileSubscribed(5_000),initialValue=emptyList())// some code}
Enter fullscreen modeExit fullscreen mode

As you see, sometimes using a repository for data exchange is justified.


So, that's all for now. Thanks for your attention :)

Top comments(6)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
rafa_b8907973ce09a8a0e663 profile image
Rafa
  • Joined

Hi Aleksei! thanks for the post, I have a doubt though: when we want to pass data to the previous screen, isn't working to do in composable2:navController.previousBackStackEntry?.savedStateHandle?.set("key", it) and then in the viewmodel1 getting the value by:savedStateHandle.get("key").
Any way to get it back there in viewmodel1? thanks!

CollapseExpand
 
mardsoul profile image
Aleksei Laptev
  • Location
    Tbilisi, Georgia
  • Work
    Android developer
  • Joined

Hello
Didn't understand. More details, pls.

CollapseExpand
 
rafa_b8907973ce09a8a0e663 profile image
Rafa
  • Joined

If you know how to navigate back from Screen2 to Screen1 and send data back. I.e.: in screen two the user types a text and once it presses system's back button, it navigates back to screen one and that text he typed earlier has to appear now in a textfield in screen1.

Thread Thread
 
mardsoul profile image
Aleksei Laptev
  • Location
    Tbilisi, Georgia
  • Work
    Android developer
  • Joined
• Edited on• Edited

You can useBackHandler, it overrides system back button. Here you are anexample with navigation with arguments.
As I mentioned in the article it's not the best practice, but like a variant... Better try another approach.

CollapseExpand
 
jhonatan_sabadi profile image
Jhonatan Sabadi
  • Joined

That's exactly what I picked! Shared viewmodel seemed easy at first, but it turned into a nightmare as it got bigger. start as a easy solution, but when it grows, becomes a mess..

CollapseExpand
 
mardsoul profile image
Aleksei Laptev
  • Location
    Tbilisi, Georgia
  • Work
    Android developer
  • Joined

Thanks for comment. I'm really glad if I helped you!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Location
    Tbilisi, Georgia
  • Work
    Android developer
  • Joined

More fromAleksei Laptev

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp