@@ -16,9 +16,13 @@ import androidx.compose.runtime.Composable
1616import androidx.compose.ui.Modifier
1717import androidx.lifecycle.lifecycleScope
1818import com.bumble.appyx.core.modality.BuildContext
19+ import com.bumble.appyx.core.navigation.NavElements
20+ import com.bumble.appyx.core.navigation.NavKey
21+ import com.bumble.appyx.core.navigation.Operation
1922import com.bumble.appyx.core.node.Node
2023import com.bumble.appyx.core.plugin.Plugin
2124import com.bumble.appyx.core.state.MutableSavedStateMap
25+ import com.bumble.appyx.core.state.SavedStateMap
2226import com.bumble.appyx.navmodel.backstack.BackStack
2327import com.bumble.appyx.navmodel.backstack.operation.pop
2428import com.bumble.appyx.navmodel.backstack.operation.push
@@ -49,6 +53,7 @@ import io.element.android.libraries.architecture.createNode
4953import io.element.android.libraries.architecture.waitForChildAttached
5054import io.element.android.libraries.core.uri.ensureProtocol
5155import io.element.android.libraries.deeplink.api.DeeplinkData
56+ import io.element.android.libraries.di.annotations.AppCoroutineScope
5257import io.element.android.libraries.featureflag.api.FeatureFlagService
5358import io.element.android.libraries.featureflag.api.FeatureFlags
5459import io.element.android.libraries.matrix.api.core.EventId
@@ -66,6 +71,7 @@ import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
6671import io.element.android.services.analytics.api.AnalyticsService
6772import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
6873import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION
74+ import kotlinx.coroutines.CoroutineScope
6975import kotlinx.coroutines.flow.distinctUntilChanged
7076import kotlinx.coroutines.flow.launchIn
7177import kotlinx.coroutines.flow.onEach
@@ -92,19 +98,27 @@ class RootFlowNode(
9298 private val announcementService : AnnouncementService ,
9399 private val analyticsService : AnalyticsService ,
94100 private val analyticsColdStartWatcher : AnalyticsColdStartWatcher ,
101+ @AppCoroutineScope private val appCoroutineScope : CoroutineScope ,
95102) : BaseFlowNode<RootFlowNode.NavTarget>(
96103 backstack = BackStack (
97104 initialElement = NavTarget .SplashScreen ,
98- savedStateMap = buildContext.savedStateMap ,
105+ savedStateMap = null ,
99106 ),
100107 buildContext = buildContext,
101108 plugins = plugins
102109) {
103110 override fun onBuilt () {
104111 analyticsColdStartWatcher.start()
105- matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
112+ appCoroutineScope.launch {
113+ matrixSessionCache.restoreWithSavedState(buildContext.savedStateMap)
114+ if (buildContext.savedStateMap != null ) {
115+ restoreSavedState(buildContext.savedStateMap)
116+ observeNavState()
117+ } else {
118+ observeNavState()
119+ }
120+ }
106121 super .onBuilt()
107- observeNavState()
108122 }
109123
110124 override fun onSaveInstanceState (state : MutableSavedStateMap ) {
@@ -119,10 +133,15 @@ class RootFlowNode(
119133 when (navState.loggedInState) {
120134 is LoggedInState .LoggedIn -> {
121135 if (navState.loggedInState.isTokenValid) {
122- tryToRestoreLatestSession(
123- onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
124- onFailure = { switchToNotLoggedInFlow(null ) }
125- )
136+ val sessionId = SessionId (navState.loggedInState.sessionId)
137+ if (matrixSessionCache.getOrNull(sessionId) != null ) {
138+ switchToLoggedInFlow(sessionId, navState.cacheIndex)
139+ } else {
140+ tryToRestoreLatestSession(
141+ onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
142+ onFailure = { switchToNotLoggedInFlow(null ) }
143+ )
144+ }
126145 } else {
127146 switchToSignedOutFlow(SessionId (navState.loggedInState.sessionId))
128147 }
@@ -134,6 +153,41 @@ class RootFlowNode(
134153 }.launchIn(lifecycleScope)
135154 }
136155
156+ /* *
157+ * Restore the saved state for navigation in the current backstack.
158+ *
159+ * **WARNING:** this is an unsafe operation abusing the internals of the Appyx library, but it's the only way allow async state
160+ * restoration and not having to block the main thread when the app starts.
161+ *
162+ * Modify with utmost care and double check any possible Appyx updates that might break this.
163+ */
164+ @Suppress(" UNCHECKED_CAST" )
165+ private fun restoreSavedState (savedStateMap : SavedStateMap ? ) {
166+ if (savedStateMap == null ) return
167+
168+ // 'NavModel' is the key used for storing the nav model state data in the map in Appyx
169+ val savedElements = buildContext.savedStateMap?.get(" NavModel" ) as ? NavElements <NavTarget , BackStack .State >
170+ if (savedElements != null ) {
171+ Timber .d(" restoreSavedElements: Saved elements found, restoring them." )
172+ backstack.accept(RestoreHistoryOperation (savedElements))
173+ }
174+ }
175+
176+ /* *
177+ * Extract the saved state for navigation in the [navTarget].
178+ *
179+ * **WARNING:** this is an unsafe operation abusing the internals of the Appyx library, but it's the only way allow async state
180+ * restoration and not having to block the main thread when the app starts.
181+ *
182+ * Modify with utmost care and double check any possible Appyx updates that might break this.
183+ */
184+ @Suppress(" UNCHECKED_CAST" )
185+ private fun extractSavedStateForNavTarget (navTarget : NavTarget , savedStateMap : SavedStateMap ? ): SavedStateMap ? {
186+ // 'ChildrenState' is the key used for storing the children state data in the map in Appyx
187+ val childrenState = savedStateMap?.get(" ChildrenState" ) as ? Map <NavKey <NavTarget >, SavedStateMap > ? : return null
188+ return childrenState.entries.find { (key, _) -> key.navTarget == navTarget }?.value
189+ }
190+
137191 private fun switchToLoggedInFlow (sessionId : SessionId , navId : Int ) {
138192 backstack.safeRoot(NavTarget .LoggedInFlow (sessionId, navId))
139193 }
@@ -202,6 +256,17 @@ class RootFlowNode(
202256 }
203257 }
204258
259+ @Parcelize
260+ class RestoreHistoryOperation <NavTarget : Any >(private val navElements : NavElements <NavTarget , BackStack .State >) : Operation<NavTarget, BackStack.State> {
261+ override fun isApplicable (elements : NavElements <NavTarget , BackStack .State >): Boolean {
262+ return true
263+ }
264+
265+ override fun invoke (existing : NavElements <NavTarget , BackStack .State >): NavElements <NavTarget , BackStack .State > {
266+ return navElements
267+ }
268+ }
269+
205270 sealed interface NavTarget : Parcelable {
206271 @Parcelize data object SplashScreen : NavTarget
207272
@@ -243,6 +308,13 @@ class RootFlowNode(
243308 backstack.push(NavTarget .NotLoggedInFlow (null ))
244309 }
245310 }
311+ val savedNavState = extractSavedStateForNavTarget(navTarget, this .buildContext.savedStateMap)
312+ val buildContext = if (savedNavState != null ) {
313+ Timber .d(" Creating a $navTarget with restored saved state" )
314+ buildContext.copy(savedStateMap = savedNavState)
315+ } else {
316+ buildContext.copy(savedStateMap = savedNavState)
317+ }
246318 createNode<LoggedInAppScopeFlowNode >(buildContext, plugins = listOf (inputs, callback))
247319 }
248320 is NavTarget .NotLoggedInFlow -> {
0 commit comments