Skip to content

Commit bbf56a8

Browse files
Save state for screen models, so that data would remain after process death
Signed-off-by: MrBoom <[email protected]>
1 parent c4b0fc8 commit bbf56a8

File tree

7 files changed

+136
-7
lines changed

7 files changed

+136
-7
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.mrboomdev.awery.gradle.util
2+
3+
import org.gradle.api.provider.ListProperty
4+
5+
operator fun <T> ListProperty<T>.plusAssign(list: List<T>) = addAll(list)

shared/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.mrboomdev.awery.gradle.ProjectVersion.getGitCommitHash
77
import com.mrboomdev.awery.gradle.generatedConfigurationsDirectory
88
import com.mrboomdev.awery.gradle.settings.GenerateSettingsTask
99
import com.mrboomdev.awery.gradle.settings.generatedSettingsDirectory
10+
import com.mrboomdev.awery.gradle.util.plusAssign
1011
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
1112
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
1213

@@ -64,6 +65,10 @@ kotlin {
6465
}
6566

6667
sourceSets {
68+
all {
69+
languageSettings.optIn("androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi")
70+
}
71+
6772
commonMain {
6873
for((_, generatedDirectory) in generations) {
6974
kotlin.srcDir(generatedDirectory.dir("kotlin"))
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.mrboomdev.awery.ui.utils
2+
3+
import android.content.ContextWrapper
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.DisposableEffect
6+
import androidx.compose.runtime.remember
7+
import androidx.compose.ui.platform.LocalContext
8+
import androidx.lifecycle.SavedStateHandle
9+
import androidx.savedstate.SavedStateRegistryOwner
10+
import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore
11+
import cafe.adriel.voyager.core.model.ScreenModel
12+
import cafe.adriel.voyager.core.model.ScreenModelStore
13+
import cafe.adriel.voyager.core.screen.Screen
14+
import kotlin.reflect.KClass
15+
16+
@Composable
17+
actual fun <T: ScreenModel> Screen.screenModel(
18+
clazz: KClass<T>,
19+
tag: String?,
20+
factory: (SavedStateHandle) -> T
21+
): T {
22+
var context = LocalContext.current
23+
24+
while(context !is SavedStateRegistryOwner) {
25+
if(context is ContextWrapper) {
26+
context = context.baseContext
27+
} else {
28+
throw UnsupportedOperationException("Activity does not extend SavedStateRegistryOwner!")
29+
}
30+
}
31+
32+
val savedStateRegistryOwner: SavedStateRegistryOwner = context
33+
val registry = savedStateRegistryOwner.savedStateRegistry
34+
35+
val savedStateKey = "screenModel;;;$key;;;savedState"
36+
val rememberKey = "$key:${clazz.qualifiedName}:${tag ?: "default"}"
37+
38+
@SuppressWarnings("RestrictedApi")
39+
val savedStateHandle = SavedStateHandle.createHandle(
40+
registry.consumeRestoredStateForKey(savedStateKey), null
41+
).apply {
42+
DisposableEffect(rememberKey) {
43+
registry.registerSavedStateProvider(savedStateKey, savedStateProvider())
44+
45+
onDispose {
46+
registry.unregisterSavedStateProvider(savedStateKey)
47+
}
48+
}
49+
}
50+
51+
val screenModelStore = remember(this) {
52+
ScreenLifecycleStore.get(this) { ScreenModelStore }
53+
}
54+
55+
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
56+
return remember(rememberKey) {
57+
screenModelStore.lastScreenModelKey.value = rememberKey
58+
@Suppress("UNCHECKED_CAST")
59+
screenModelStore.screenModels.getOrPut(rememberKey) {
60+
factory(savedStateHandle)
61+
} as T
62+
}
63+
}

shared/src/commonMain/kotlin/com/mrboomdev/awery/ui/routes/SearchRoute.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,16 @@ import androidx.compose.ui.text.TextStyle
3636
import androidx.compose.ui.text.style.TextAlign
3737
import androidx.compose.ui.unit.dp
3838
import androidx.compose.ui.unit.sp
39+
import androidx.lifecycle.SavedStateHandle
40+
import androidx.lifecycle.viewmodel.compose.saveable
3941
import cafe.adriel.voyager.core.model.ScreenModel
40-
import cafe.adriel.voyager.core.model.rememberScreenModel
4142
import cafe.adriel.voyager.navigator.LocalNavigator
4243
import cafe.adriel.voyager.navigator.currentOrThrow
4344
import com.mrboomdev.awery.ext.data.Setting
4445
import com.mrboomdev.awery.generated.*
4546
import com.mrboomdev.awery.sources.ExtensionsManager
4647
import com.mrboomdev.awery.ui.utils.LocalToaster
48+
import com.mrboomdev.awery.ui.utils.screenModel
4749
import kotlinx.serialization.Contextual
4850
import kotlinx.serialization.Serializable
4951
import org.jetbrains.compose.resources.painterResource
@@ -61,12 +63,12 @@ open class DefaultSearchRoute(
6163
@Composable
6264
override fun Content() {
6365
val navigation = LocalNavigator.currentOrThrow
64-
val model = rememberScreenModel { SearchModel() }
66+
val screenModel = screenModel { SearchModel(it) }
6567
val queryFocusRequested = remember { FocusRequester() }
6668

6769
val foundSources by remember { derivedStateOf {
6870
ExtensionsManager.allSources.filter { source ->
69-
model.query.text.trim().let { text ->
71+
screenModel.query.text.trim().let { text ->
7072
source.context.name?.contains(text) == true || source.context.id.contains(text)
7173
}
7274
}.sortedBy { it.context.name ?: it.context.id }
@@ -103,7 +105,7 @@ open class DefaultSearchRoute(
103105
.padding(4.dp)
104106
.fillMaxWidth(),
105107

106-
state = model.query,
108+
state = screenModel.query,
107109
lineLimits = TextFieldLineLimits.SingleLine,
108110
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
109111

@@ -168,6 +170,6 @@ open class DefaultSearchRoute(
168170
}
169171
}
170172

171-
private class SearchModel: ScreenModel {
172-
val query = TextFieldState()
173+
private class SearchModel(savedState: SavedStateHandle): ScreenModel {
174+
val query by savedState.saveable(saver = TextFieldState.Saver) { TextFieldState() }
173175
}

shared/src/commonMain/kotlin/com/mrboomdev/awery/ui/screens/SplashScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ fun SplashScreen(
188188
}
189189
}
190190

191-
LoadingStatus.Finished -> stringResource(Res.string.status_finished)
191+
LoadingStatus.Finished -> ""
192192
}
193193
)
194194
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.mrboomdev.awery.ui.utils
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.lifecycle.SavedStateHandle
5+
import cafe.adriel.voyager.core.model.ScreenModel
6+
import cafe.adriel.voyager.core.screen.Screen
7+
import kotlin.reflect.KClass
8+
9+
@Composable
10+
inline fun <reified T: ScreenModel> Screen.screenModel(
11+
tag: String? = null,
12+
noinline factory: (SavedStateHandle) -> T
13+
) = screenModel(T::class, tag, factory)
14+
15+
/**
16+
* This function allows us to use an [SavedStateHandle] to restore state after an process death :)
17+
*/
18+
@Composable
19+
expect fun <T: ScreenModel> Screen.screenModel(
20+
clazz: KClass<T>,
21+
tag: String? = null,
22+
factory: (SavedStateHandle) -> T
23+
): T
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.mrboomdev.awery.ui.utils
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.remember
5+
import androidx.lifecycle.SavedStateHandle
6+
import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore
7+
import cafe.adriel.voyager.core.model.ScreenModel
8+
import cafe.adriel.voyager.core.model.ScreenModelStore
9+
import cafe.adriel.voyager.core.screen.Screen
10+
import kotlin.reflect.KClass
11+
12+
// On desktop system don't kill your app, so we may just use a regular rememberScreenModel
13+
@Composable
14+
actual fun <T: ScreenModel> Screen.screenModel(
15+
clazz: KClass<T>,
16+
tag: String?,
17+
factory: (SavedStateHandle) -> T
18+
): T {
19+
val screenModelStore = remember(this) {
20+
ScreenLifecycleStore.get(this) { ScreenModelStore }
21+
}
22+
23+
val rememberKey = "$key:${clazz.qualifiedName}:${tag ?: "default"}"
24+
25+
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
26+
return remember(rememberKey) {
27+
screenModelStore.lastScreenModelKey.value = rememberKey
28+
@Suppress("UNCHECKED_CAST")
29+
screenModelStore.screenModels.getOrPut(rememberKey) { factory(SavedStateHandle()) } as T
30+
}
31+
}

0 commit comments

Comments
 (0)