Skip to content

Commit 0e64d9c

Browse files
committed
Remove runBlocking call to restore sessions when the app starts
Sadly, to do this we need to manually handle restoring the state from Appyx using internal values. At least it doesn't seem like they're going to change any time soon (or ever). This should take care of a few ANRs, although it may make loading the initial state a bit slower
1 parent 8c20770 commit 0e64d9c

File tree

2 files changed

+83
-14
lines changed

2 files changed

+83
-14
lines changed

appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import androidx.compose.runtime.Composable
1616
import androidx.compose.ui.Modifier
1717
import androidx.lifecycle.lifecycleScope
1818
import 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
1922
import com.bumble.appyx.core.node.Node
2023
import com.bumble.appyx.core.plugin.Plugin
2124
import com.bumble.appyx.core.state.MutableSavedStateMap
25+
import com.bumble.appyx.core.state.SavedStateMap
2226
import com.bumble.appyx.navmodel.backstack.BackStack
2327
import com.bumble.appyx.navmodel.backstack.operation.pop
2428
import com.bumble.appyx.navmodel.backstack.operation.push
@@ -49,6 +53,7 @@ import io.element.android.libraries.architecture.createNode
4953
import io.element.android.libraries.architecture.waitForChildAttached
5054
import io.element.android.libraries.core.uri.ensureProtocol
5155
import io.element.android.libraries.deeplink.api.DeeplinkData
56+
import io.element.android.libraries.di.annotations.AppCoroutineScope
5257
import io.element.android.libraries.featureflag.api.FeatureFlagService
5358
import io.element.android.libraries.featureflag.api.FeatureFlags
5459
import io.element.android.libraries.matrix.api.core.EventId
@@ -66,6 +71,7 @@ import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
6671
import io.element.android.services.analytics.api.AnalyticsService
6772
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
6873
import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION
74+
import kotlinx.coroutines.CoroutineScope
6975
import kotlinx.coroutines.flow.distinctUntilChanged
7076
import kotlinx.coroutines.flow.launchIn
7177
import 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 -> {

appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
2121
import io.element.android.libraries.matrix.api.core.SessionId
2222
import io.element.android.services.analytics.api.AnalyticsService
2323
import io.element.android.services.analyticsproviders.api.AnalyticsUserData
24-
import kotlinx.coroutines.runBlocking
2524
import kotlinx.coroutines.sync.Mutex
2625
import kotlinx.coroutines.sync.withLock
2726
import timber.log.Timber
@@ -77,20 +76,18 @@ class MatrixSessionCache(
7776
}
7877

7978
@Suppress("UNCHECKED_CAST")
80-
fun restoreWithSavedState(state: SavedStateMap?) {
79+
suspend fun restoreWithSavedState(state: SavedStateMap?) {
8180
Timber.d("Restore state")
8281
if (state == null || sessionIdsToMatrixSession.isNotEmpty()) {
83-
Timber.w("Restore with non-empty map")
82+
Timber.w("No need to restore saved state")
8483
return
8584
}
8685
val sessionIds = state[SAVE_INSTANCE_KEY] as? Array<SessionId>
8786
Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}")
8887
if (sessionIds.isNullOrEmpty()) return
8988
// Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs.
90-
runBlocking {
91-
sessionIds.forEach { sessionId ->
92-
getOrRestore(sessionId)
93-
}
89+
sessionIds.forEach { sessionId ->
90+
getOrRestore(sessionId)
9491
}
9592
}
9693

0 commit comments

Comments
 (0)