Hello! Is there a way to persist the data of a vie...
# compose
e
Hello! Is there a way to persist the data of a view model after navigating to one screen and navigating back? For example, I am on screen A and I load some data, I navigate to screen B, I use
navigateUp()
. I expect to not have to load the data again in Screen A as the data in the view model is persisted.
i
The only reason data you've loaded on screen A would be reloaded when you come back to screen A is because you told it to reload it every time your screen enters composition so don't do that if you don't want that to happen. Perhaps you can share your code?
c
Yep ^ what Ian said. Typically if I have screen A -> B-> -> C then go back to B then A, nothing has to reload because the ViewModel is still around during the navigation down into C. Sounds like something is causing some work to take place
j
Not sure if I understand correctly, but if you want to persist data due to saving api calls then you can use repository as state holder.
i
Your ViewModel exists for the entire time your screen is on the back stack, so caching at a repository layer underneath your ViewModel doesn't actually save you anything for the in memory case (persisting to disk is a whole separate thing)
☝️ 2
e
My navigation is a bit particular and complex (probably I could improve it) but I will explain my implementation. I have a
NavHost
(let's call it
MainNavController
) that includes a
ModalNavigationDrawer
, let's call it
ScreenContainer
, and then I have at the same lavel all the screens that are supposed to be detail screens, let's all them
Screen B
and
Screen C
Copy code
val mainNavController = rememberNavController()

    val navigateTo: (uri: String) -> Unit = remember { { uri -> navigate(mainNavController, uri) } }
    val goBack: () -> Boolean = remember { mainNavController::navigateUp }

    NavHost(navController = mainNavController, startDestination = "Screen Container") {
        composable(route = "Screen B") {
            ScreenB(navigateBack = goBack)
        }
        composable(route = "Screen C") {
            ScreenC(navigateBack = goBack)
        }
        composable(route = "Screen Container") {
            val vm: ContainerViewModel = koinInject()
            val uiState by vm.uiState.collectAsStateWithCurrentLifecycle()
            val contentNavController = rememberNavController()

            LaunchedEffect("InitScreenContainer") {
                vm.initData()
            }

            ScreenContainer(
                contentNavController = contentNavController,
                uiState = uiState,
                ...
                navigate = navigateTo,
            )
        }
    }
}
The thing is that
ScreenContainer
contains a
ModalNavigationDrawer
, an
ActionBar
and inside, the content, which is another
NavHost
(let's call it
ContentNavController
) with few screens, let's call them
Screen A1
and
Screen A2
. This screens are rendered when using the Navigation Drawer menu, inside the internal `NavHost`(
ContentNavController
)
Copy code
ScreenContainer(contentNavController: NavHostController, navigate: (uri: String) -> Unit, ...) {
     Scaffold(topBar = { ... }) {
          NavHost(
                  navController = navController,
                  startDestination = "Screen A1",
          ) {
                  composable(route = "Screen A1") {
                      ScreenA1(navigate = navigate)
                  }
                  composable(route = "Screen A2") {
                      ScreenA2(navigate = navigate)
                  }
          }
     }
}
The viewmodels of the screens are hosted inside the composable like this:
Copy code
ScreenA1(...) {
     val vm: ScreenA1ViewModel = koinInject()
     ...
}
So now, let's say that in the
MainNavController
I am in
ScreenContainer
which internally, in the
ContentNavController
is in
Screen A1
I navigate from
Screen A1
to
Screen A2
in the
ContentNavController
. From the
Screen A2
I navigate to
Screen B
in the
MainNavController
We have in this case two stacks: • MainNavController:
ScreenContainer
>
Screen B
• ContentNavController:
Screen A1
>
Screen A2
My expectations when I
navigateUp()
from
Screen B
is that I am back in
ScreenContainer
with the
Screen A2
inflated, and
Screen A2
has the data loaded inside. My current status is that when I
navigateUp()
from
Screen B
, I am back in
ScreenContainer
with the
Screen A2
inflated, but the Viewmodel is re instantiated. I know it can be difficult to understand, thank you for your help in advance, any suggestion in how to simplify my current logic is welcome, I understand that is difficult without seeing the actual code though.
s
So the entire NavHost which lives inside "Screen Container" leaves composition then right? So it just re-instates itself every time you go back there, since you make it leave composition completely, so you get a fresh ViewModel as a consequence too. Why do you have two NavHosts in the first place? The "Screen Container" destination could be a graph with "Screen A1" "Screen A2 etc as destinations inside of it. Then to navigate to "Screen B" you could pop the entire graph with
saveState = true
, and when you come back do
restoreState = true
, and you will have all the same functionality without the nested NavHost which is definitely not really a good idea most of the times. What am I missing here which does not let you do that? edit: Oh, is is that this separate NavHost lives inside a drawer? 👀
e
Yes, the main problem is that
Screen A1
and
Screen A2
are inside the NavigationDrawer and the action bar. While
Screen B
and
Screen C
are OVER the navigation drawer and they have their own ActionBar with the navigation back button, instead opening the NavigationDrawer. That's why I have the nested NavHost. To explain it in an old school way,
Screen A1
and
Screen A2
"are fragments" inside one activity, and
Screen B
and
Screen C
"are different activities".
s
I must admit it's a bit hard to visualize, is the app public? Could you share a video of you navigating between all of those screens in the app?
e
Sadly it is not public, I will try to record it later, thanks for your help!
i
Copy code
LaunchedEffect("InitScreenContainer") {
  vm.initData()
}
This is what is force reloading your data every time you come back to the screen
c
> If you want to do something one time, it can't be part of composition at all, it should be done in the ViewModel e.g., in the
init
or a
by lazy
Technically... Could you have a launchEffect keyed on the VM?
i
No, because every
LaunchedEffect
is going to rerun when it re-enters composition - that's the exact problem here
1
c
ah true. this goes back to what you said about "init" not necessarily being bad, but having an init which interacts with Dispatchers.main.immediate while touching state is bad. (i may have misrepresented what you said, but that was sorta the gist i think)
The cold flow +
stateIn
+
WhileSubscribed
is generally what you should reach for 99% of the time - repositories deliver cold flows of information, your ViewModel caches those flows with
stateIn
, then your UI collects those when it enters composition
👌 1
e
Thank you! I will need to refactor a lot for achievin that, overall because of the unit tests, but I will grive it a try!