https://kotlinlang.org logo
#compose
Title
# compose
a

Archie

11/22/2020, 3:06 PM
In all applications there will always be this three scopes of state:
With Compose, a "Per Screen State" could be achieved by:
Copy code
NavHost(navController, startDestination = startRoute) {
    ...
    composable(route) {
       ...
       val perScreenViewModel = viewModel()  // This will be different from
    }
    composable(route) {
       ...
       val perScreenViewModel = viewModel()  // this instance
    }
    ...
}
The "App State" could be achieved by:
Copy code
val appStateViewModel = viewModel()
NavHost(navController, startDestination = startRoute) {
    ...
}
But how about for "Scoped State"? How could we achieve it in Compose?
b

Bradleycorn

11/22/2020, 4:15 PM
There’s probably some issues here, and there’s probably a much more elegant solution. but something VERY basic that might get you started:
Copy code
// Created a sealed class that represents your different scopes
sealed class Scope {
    object Scope1: Scope()
    object Scope2: Scope()
}

// Created a "ScopedViewModel" class with a boolean so we can know if it has been cleared/destroyed already.
open class ScopedViewModel: ViewModel() {
    var isDestoryed: Boolean = false
        private set
    override fun onCleared() {
        super.onCleared()
        isDestoryed = true
    }
}

// Setup viewmodels that inherit from ScopedViewModel
class Scope1ViewModel(): ScopedViewModel() { //... }
class Scope2ViewModel(): ScopedViewModel() { //...  }

//Setup an object that keeps track of which scoped viewmodels have been created,
//and can create and return them when necessary
object MyScopedViewModels {

    private val viewModels: MutableMap<Scope, ScopedViewModel> = mutableMapOf()

    fun getViewModel(scope: Scope): ScopedViewModel {
        val viewModel = viewModels[scope]

        return when {
            viewModel == null -> createViewModel(scope)
            viewModel.isDestoryed -> createViewModel(scope)
            else -> viewModel
        }
    }

    private fun createViewModel(scope: Scope): ScopedViewModel {
        val viewModel = when (scope) {
            Scope.Scope1 -> Scope1ViewModel()
            Scope.Scope2 -> Scope2ViewModel()
        }

        viewModels[scope] = viewModel
        return viewModel
    }
}
The above solves the basic issue of providing the same view model for say, Screen1, Screen2, and Screen3 … But, it still has a problem where it doesn’t really know if/when it goes out of scope (ie. when Screen1, Screen2, and/or Screen3 are all removed from the composed tree), and to destroy itself. So if you navigate to screen 1, and then 3, great you get the same instance of the view model (call it Scope1). then you pop 1 and 3 from the backstack and go to screen 4, great you get a different viewmodel for screen 4. But then if you pop4 and navigate to screen 3 again, you won’t get a new instance of Scope1, you’ll get the “old” instance of Scope1 that was created previously.
As of today, I don’t know that there is a great way to do it. You’d have to write something to manually monitor the navcontroller/backstack, and figure out that none of the screens that make up a “scope” are in the tree and then destroy the viewmodel that goes with that scope. I believe that I’ve read in a few different places that the Compsed team is putting a lot of serious thought into how to properly setup and enable scoping of ViewModels to a screen (or set of screens), but there’s nothing that’s been released yet.
Another thought is to use something like your first example above, but lump each set of screens that make up a scope into a single “Scope” destination, and use an argument on the route to determine which “screen” composable to show. Though I think the whole “per screen” setup is still potentially problematic. Even though you are creating a new instance of the viewmodel each time the screen is navigated to, that view model is still scoped to the Activity/Fragment that it is created in, so if you navigate to the same screen several times, it’ll create a new instance of the viewmodel sure, but the old instance(s) still exist because their scope (the Activity/Fragment is still active). So, you still have that problem, but you could create a
Scope
composable that gets a viewmodel and composes one of a set of screens:
Copy code
NavHost(navController, startDestination = startRoute) {
    ...
    composable("/scope1/{screen}",
     arguments = listOf(navArgument("screen") { type = Nattype.StringType })
    ) { backStackEntry ->
       ...
       val perScopeiewModel = viewModel()  // This will be different from
       val screen = backStackEntry.arguments?.getString("screen") ?: "Screen1"
       Scope1(perScopeViewModel, screen)
    }
    composable(route) {
       ...
       val perScreenViewModel = viewModel()  // this instance
    }
    ...
}

....

@Composable
fun Scope1(viewModel: ViewModel, screen: String) {
    when (screen) {
      "Screen1" -> Screen1(viewModel)
      "Screen2" -> Screen2(viewModel)
      "Screen3" -> Screen3(viewModel)
      else -> throw IllegalArgumentException("Screen $screen is not part of Scope 1") 
}
Still not great, but maybe workable. Also, 1.0.0-alpha02 of the navigation component supports nested nav graphs. I haven’t used them yet, but that might offer a way to do something where you can create the viewmodel in the higherlevel
compose()
for the scope, and then pass it along to each nested screen. But again, I haven’t used it yet, so can’t say if or how well that would work.
a

allan.conda

11/22/2020, 6:24 PM
there's nav graph scoped viewmodels from jetpack nav. still not supported in compose navigator tho i think
also rn all viewmodels are scoped to the activity/fragment so they kinda stay forever. scoping within composables is still wip
f

florent

11/22/2020, 6:55 PM
Just use no fragments at all and multiple activities that's much more simplier than that
m

Mark Murphy

11/23/2020, 1:32 PM
@Ian Lake wrote a nice answer to this at https://stackoverflow.com/a/64961032/115145, though it ends on a cliffhanger
😂 1
n

nickbutcher

11/23/2020, 4:05 PM
USING WHAT @Ian Lake???
b

Bradleycorn

11/23/2020, 4:05 PM
haha
i

Ian Lake

11/23/2020, 4:48 PM
Cliff hanger removed (I had incorporated it into a comment in the code, so you weren't missing anything)
2 Views