Bit of an architecture question if anybody has a s...
# compose
n
Bit of an architecture question if anybody has a sec? I have some temporary state which should be shared between (and updated by) two distinct screens. It’s not a repository level thing as its just some temporary state. Trying to find the best way to get this state into the relevant screen view model. Explored options in thread
Ideal Solution: Scope to the backstack entry and “somehow” inject into the individual view models
1) Merge the viewmodels for the two screens into one and have that be shared. Pros: Simple solution Cons: We end up with a huge view model which is mixed up with logic for two different screens.
2) Pass the shared state down into the screens and then back up into their respective view models. Cons: Feels a bit against the grain in terms of data flow
Not sure if I’m missing something really obvious but the way I imagine the structure is Shared State / \ VM1 VM2
s
What type of data is in your state?
n
It’s a fitness app, screen 1 you have your workout and can update the weights you wish to use for that session. Screen two you are then performing that workout (and potentially still updating the weights). In terms of specific data models The state is an object which represents the workout and a list inside with forces etc. The values that can be updated are Snapshot State objects.
k
You can have a screen-scoped ViewModel for each screens And a nav-graph-scoped ViewModel for both screens Both screens are in a nested navigation
☝️ 1
s
I know you said its not a repository level thing, but just because you're not fetching from a network or something doesn't mean you can't use some structure for returning state across screens. Even if its some in memory singleton type thing
n
@Kefas so the main issue with that approach (similar to my second suggestion above) is that we want that shared state to be available in the viewmodels not just the screens.
@Scott Kruse I guess thats possible, just scoping it correctly would be the complicated part i guess.
i
It certainly sounds like you want an in progress workout manager that you can inject into both ViewModels
👍 3
k
@Nathan Castlehow Currently my implementation for that problem is to inject the shared ViewModel into the screen-scoped view model.
Copy code
class FragmentScopedVM(private val navGraphScopedVM: NavGraphScopedVM)
I'm not really sure if this is a good practice though.
n
Agreed it does need to sit above the two view models and get injected. I think the scoping for that is the hard part. Ideally its scoped to somewhere in the nav stack but I don’t think thats possible with the structure we are suggesting? (unless I’m missing something really obvious)
@Kefas how are you scoping your navgraphscopedvm to the navgraph? and then having it injected?
k
Copy code
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "nested") {
    navigation(startDestination = "ScreenA", route = "nested") {
        composable(route = "ScreenA") { navBackStackEntry ->
            val sharedViewModel: SharedViewModel =
                viewModel(navController.getBackStackEntry(navBackStackEntry.destination.parent!!.id))
            val screenAViewModel: ScreenAViewModel =
                viewModel(factory = object : ViewModelProvider.Factory {
                    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                        return ScreenAViewModel(sharedViewModel) as T
                    }
                })
        }
        composable(route = "ScreenB") { TODO() }
    }
}
If you're using fragment, you can just use
Fragment.navGraphViewModels
n
hmmm is there a way to scope it with hilt view model injection instead of having to create a custom factory?
i
hiltViewModel
has the same ability to scope ViewModels
n
Firstly, a quick thanks for your help. Its very much appreciated! So for me the main issue is around the scoping of the inprogress workout manager. The view models are correctly scoped due to being created via a hiltViewModel call. Their paramaters are injected so when creating the view model I can just call hiltViewModel() The inprogress workout manager should exist only when either one of those view models exist. My impression is that I would need to either 1) Write some sort of hilt provider / scope to manage the inprogressworkoutmanager 2) Create the view models manually at the point they are used so i can pass in an inprogressworkoutmanager scoped Is that correct? Basically I can’t see any existing hilt scope that would have the correct lifetime.
Last bit of follow up for anyone who comes across this again. I ended up giving the in progress workout manager a wider scope than it needed to force it to sit above the two view models. Not exactly technical perfection but pragmatic based on constraints (technical and otherwise)
👍 1
j
@Nathan Castlehow 1. Did you end up making the “in progress workout manager” a singleton? 2. Does this setup prevent re-usability of the screens? I mean: ScreenA and ScreenB now expect to share a data structure (through their viewmodels). Would it be possible to reuse ScreenB as-is and make it work with an hypothetical ScreenA2?
n
@julioromano I made the scope slightly tighter than singleton but its the same basic gist. The in progress workout manager understands if its been initialised or not and holds its own data so isn’t too tightly coupled to the screens (minus the fact it needs to get “uninitalized” at some point by a screen). If screen B accesses it before screen A thats fine because the in progress workout manager understands its not initialized and does the relevant setup.
👍 1