From 767f408a5ea49f7370c80cb4099a5259107dea4e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 14:47:29 +0300 Subject: [PATCH 01/15] fix task dependency for copy wasm resources workaround --- sample/wasmApp/build.gradle.kts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sample/wasmApp/build.gradle.kts b/sample/wasmApp/build.gradle.kts index 454df61c..8861dd60 100644 --- a/sample/wasmApp/build.gradle.kts +++ b/sample/wasmApp/build.gradle.kts @@ -14,9 +14,12 @@ val copyWasmResources = tasks.create("copyWasmResourcesWorkaround", Copy::class. } afterEvaluate { - project.tasks.getByName("wasmJsProcessResources").finalizedBy(copyWasmResources) - project.tasks.getByName("wasmJsDevelopmentExecutableCompileSync").dependsOn(copyWasmResources) - project.tasks.getByName("wasmJsProductionExecutableCompileSync").dependsOn(copyWasmResources) + tasks { + getByName("wasmJsProcessResources").finalizedBy(copyWasmResources) + getByName("wasmJsDevelopmentExecutableCompileSync").dependsOn(copyWasmResources) + getByName("wasmJsProductionExecutableCompileSync").dependsOn(copyWasmResources) + getByName("wasmJsJar").dependsOn(copyWasmResources) + } } kotlin { From 28874c5f9de38a0788e6ba5c3f3b1d9508a48a3c Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 15:17:28 +0300 Subject: [PATCH 02/15] add selection container to code blocks --- .../flowmvi/sample/ui/widgets/CodeView.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt index d1be9030..6c310502 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -67,15 +68,17 @@ fun CodeText( }.annotatedString } Box(modifier = modifier.horizontalScroll(rememberScrollState())) { - Text( - text = string, - fontSize = 13.sp, - fontFamily = FontFamily.Monospace, // TODO: Monaspace appears to be unsupported by compose? - textAlign = TextAlign.Start, - overflow = TextOverflow.Visible, - softWrap = false, - lineHeight = 16.sp, - modifier = Modifier.fillMaxWidth(), - ) + SelectionContainer { + Text( + text = string, + fontSize = 13.sp, + fontFamily = FontFamily.Monospace, // TODO: Monaspace appears to be unsupported by compose? + textAlign = TextAlign.Start, + overflow = TextOverflow.Visible, + softWrap = false, + lineHeight = 16.sp, + modifier = Modifier.fillMaxWidth(), + ) + } } } From 6b34461fc55e5eb95c8d685765d1db7af694d01f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 16:57:52 +0300 Subject: [PATCH 03/15] refactor savedState module to support localstorage on browser platforms --- .../src/main/kotlin/ConfigureMultiplatform.kt | 7 +- .../pro/respawn/flowmvi/util/TypeExt.kt | 1 + savedstate/build.gradle.kts | 45 ++++++-- .../savedstate/dsl/Compress.android.kt | 19 ---- .../savedstate/platform/FileAccess.android.kt | 51 +++++++++ .../flowmvi/savedstate/dsl/Compress.kt | 22 ---- .../flowmvi/savedstate/dsl/FileSaver.kt | 53 +++------- .../flowmvi/savedstate/platform/FileAccess.kt | 10 ++ .../savedstate/plugins/SavedStatePlugin.kt | 5 +- .../plugins/SerializeStatePlugin.kt | 33 +++--- .../respawn/flowmvi/savedstate/util/Util.kt | 12 +++ .../flowmvi/savedstate/dsl/Compress.js.kt | 6 -- .../savedstate/platform/FileAccess.js.kt | 16 +++ .../flowmvi/savedstate/dsl/Compress.jvm.kt | 21 ---- .../savedstate/platform/FileAccess.jvm.kt | 50 +++++++++ .../flowmvi/savedstate/dsl/Compress.native.kt | 6 -- .../savedstate/platform/FileAccess.native.kt | 34 ++++++ .../savedstate/dsl/DefaultFileSaver.kt | 76 +++++++++++++ .../SerializeStatePlugin.nonBrowser.kt | 100 ++++++++++++++++++ .../flowmvi/savedstate/dsl/Compress.wasmJs.kt | 8 -- .../savedstate/platform/FileAccess.wasmJs.kt | 16 +++ 21 files changed, 444 insertions(+), 147 deletions(-) delete mode 100644 savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.android.kt create mode 100644 savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt delete mode 100644 savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.kt create mode 100644 savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.kt delete mode 100644 savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.js.kt create mode 100644 savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.js.kt delete mode 100644 savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.jvm.kt create mode 100644 savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt delete mode 100644 savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.native.kt create mode 100644 savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.native.kt create mode 100644 savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/DefaultFileSaver.kt create mode 100644 savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt delete mode 100644 savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.wasmJs.kt create mode 100644 savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.wasmJs.kt diff --git a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt index b848a7f9..f354c667 100644 --- a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt +++ b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt @@ -3,10 +3,12 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.getting +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyBuilder import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl -@OptIn(ExperimentalWasmDsl::class) +@OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class) fun Project.configureMultiplatform( ext: KotlinMultiplatformExtension, jvm: Boolean = true, @@ -20,10 +22,11 @@ fun Project.configureMultiplatform( windows: Boolean = true, wasmJs: Boolean = true, wasmWasi: Boolean = false, // TODO: Coroutines do not support wasmWasi yet + configure: KotlinHierarchyBuilder.Root.() -> Unit = {}, ) = ext.apply { val libs by versionCatalog explicitApi() - applyDefaultHierarchyTemplate() + applyDefaultHierarchyTemplate(configure) withSourcesJar(true) if (linux) { diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/TypeExt.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/TypeExt.kt index acb1698e..da607a38 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/TypeExt.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/TypeExt.kt @@ -27,4 +27,5 @@ public inline fun Any?.typed(): T? = this as? T /** * Get the name of the class, removing the "State" suffix, if present. */ +@Deprecated("Usage of this function leads to some unintended consequences when enabling code obfuscation") public inline fun nameByType(): String? = T::class.simpleName?.removeSuffix("State") diff --git a/savedstate/build.gradle.kts b/savedstate/build.gradle.kts index a21d3230..af76ade5 100644 --- a/savedstate/build.gradle.kts +++ b/savedstate/build.gradle.kts @@ -1,17 +1,44 @@ plugins { - id("pro.respawn.shared-library") + kotlin("multiplatform") alias(libs.plugins.serialization) + id("com.android.library") + id("maven-publish") + signing +} + +kotlin { + configureMultiplatform(this) { + common { + group("nonBrowser") { + withJvm() + withNative() + withAndroidTarget() + } + group("browser") { + withWasm() + withJs() + } + } + } + + sourceSets { + nativeMain.dependencies { + implementation(libs.kotlin.io) + } + androidMain.dependencies { + api(libs.lifecycle.savedstate) + } + commonMain.dependencies { + api(projects.core) + api(libs.kotlin.serialization.json) + implementation(libs.kotlin.atomicfu) + } + } } android { + configureAndroidLibrary(this) namespace = "${Config.namespace}.savedstate" } -dependencies { - commonMainApi(projects.core) - commonMainApi(libs.kotlin.serialization.json) - commonMainImplementation(libs.kotlin.atomicfu) - commonMainImplementation(libs.kotlin.io) - - androidMainApi(libs.lifecycle.savedstate) -} +publishMultiplatform() diff --git a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.android.kt b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.android.kt deleted file mode 100644 index 28e0d3db..00000000 --- a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.android.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pro.respawn.flowmvi.savedstate.dsl - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.io.files.Path -import java.io.FileInputStream -import java.io.FileOutputStream -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream - -@Suppress("INVISIBLE_MEMBER") // we want to access the JVM api of kotlinx.io... -internal actual suspend fun writeCompressed(data: String, to: Path) = withContext(Dispatchers.IO) { - GZIPOutputStream(FileOutputStream(to.file)).writer().use { it.write(data) } -} - -@Suppress("INVISIBLE_MEMBER") // we want to access the JVM api of kotlinx.io... -internal actual suspend fun readCompressed(from: Path): String? = withContext(Dispatchers.IO) { - GZIPInputStream(FileInputStream(from.file)).reader().use { it.readText() } -} diff --git a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt new file mode 100644 index 00000000..6725a643 --- /dev/null +++ b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt @@ -0,0 +1,51 @@ +package pro.respawn.flowmvi.savedstate.platform + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.InputStream +import java.io.InputStreamReader +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +@PublishedApi +internal actual object FileAccess { + + private fun File.outputStreamOrEmpty() = run { + mkdirs() + createNewFile() + outputStream() + } + + private fun InputStream.readOrNull() = reader().use { it.readText() }.takeIf { it.isNotBlank() } + + actual suspend fun writeCompressed(data: String?, path: String) = withContext(Dispatchers.IO) { + val file = File(path) + if (data == null) { + file.delete() + return@withContext + } + file.outputStreamOrEmpty().let(::GZIPOutputStream).writer().use { it.write(data) } + } + + actual suspend fun readCompressed(path: String): String? = withContext(Dispatchers.IO) { + val file = File(path) + if (!file.exists()) return@withContext null + file.inputStream().let(::GZIPInputStream).readOrNull() + } + + actual suspend fun write(data: String?, path: String) = withContext(Dispatchers.IO) { + val file = File(path) + if (data == null) { + file.delete() + return@withContext + } + file.outputStreamOrEmpty().writer().use { it.write(data) } + } + + actual suspend fun read(path: String): String? = withContext(Dispatchers.IO) { + val file = File(path) + if (!file.exists()) return@withContext null + file.inputStream().readOrNull() + } +} diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.kt deleted file mode 100644 index 58c82626..00000000 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.kt +++ /dev/null @@ -1,22 +0,0 @@ -package pro.respawn.flowmvi.savedstate.dsl - -import kotlinx.io.buffered -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem -import kotlinx.io.readString -import kotlinx.io.writeString - -internal expect suspend fun writeCompressed(data: String, to: Path) -internal expect suspend fun readCompressed(from: Path): String? - -@OptIn(ExperimentalStdlibApi::class) -internal fun write(data: String, to: Path) { - SystemFileSystem.sink(to).buffered().use { it.writeString(data) } -} - -@OptIn(ExperimentalStdlibApi::class) -internal fun read(from: Path): String? = SystemFileSystem - .source(from) - .buffered() - .use { it.readString() } - .takeIf { it.isNotBlank() } diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/FileSaver.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/FileSaver.kt index 47bf8156..bb61b5e5 100644 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/FileSaver.kt +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/FileSaver.kt @@ -4,10 +4,9 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem import pro.respawn.flowmvi.savedstate.api.Saver import pro.respawn.flowmvi.savedstate.api.ThrowRecover +import pro.respawn.flowmvi.savedstate.platform.FileAccess /** * A [Saver] implementation that saves the given state to a file in a specified [dir] and [fileName]. @@ -20,38 +19,24 @@ import pro.respawn.flowmvi.savedstate.api.ThrowRecover * * The writes to the file cannot be canceled to prevent saving partial data. */ public inline fun DefaultFileSaver( - dir: String, - fileName: String, - crossinline write: suspend (data: T, to: Path) -> Unit, - crossinline read: suspend (from: Path) -> T?, + path: String, + crossinline write: suspend (data: T?, toPath: String) -> Unit, + crossinline read: suspend (fromPath: String) -> T?, crossinline recover: suspend (Exception) -> T?, ): Saver = object : Saver { - val directory = Path(dir) - val file = Path(directory, fileName) // prevent concurrent file access private val mutex = Mutex() override suspend fun recover(e: Exception): T? = recover.invoke(e) - override suspend fun save( - state: T? - ) = withContext(NonCancellable) { // prevent partial writes - mutex.withLock { - with(SystemFileSystem) { - if (state == null) { - delete(file, false) - } else { - createDirectories(directory) - write(state, file) - } - } - } + + // prevent partial writes + override suspend fun save(state: T?) = withContext(NonCancellable) { + mutex.withLock { write(state, path) } } // allow cancelling reads (no "NonCancellable here") - override suspend fun restore(): T? = mutex.withLock { - file.takeIf { SystemFileSystem.exists(file) }?.let { read(it) } - } + override suspend fun restore(): T? = mutex.withLock { read(path) } } /** @@ -65,15 +50,13 @@ public inline fun DefaultFileSaver( * @see Saver */ public fun FileSaver( - dir: String, - fileName: String, + path: String, recover: suspend (Exception) -> String? = ThrowRecover, ): Saver = DefaultFileSaver( - dir = dir, - fileName = fileName, + path = path, recover = recover, - write = ::write, - read = ::read, + write = FileAccess::write, + read = FileAccess::read, ) /** @@ -90,13 +73,11 @@ public fun FileSaver( * @see Saver */ public fun CompressedFileSaver( - dir: String, - fileName: String, + path: String, recover: suspend (Exception) -> String? = ThrowRecover, ): Saver = DefaultFileSaver( - dir = dir, - fileName = fileName, + path = path, recover = recover, - write = ::writeCompressed, - read = ::readCompressed, + write = FileAccess::writeCompressed, + read = FileAccess::readCompressed, ) diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.kt new file mode 100644 index 00000000..ccdc7e71 --- /dev/null +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.kt @@ -0,0 +1,10 @@ +package pro.respawn.flowmvi.savedstate.platform + +@PublishedApi +internal expect object FileAccess { + + suspend fun writeCompressed(data: String?, path: String) + suspend fun readCompressed(path: String): String? + suspend fun write(data: String?, path: String) + suspend fun read(path: String): String? +} diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SavedStatePlugin.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SavedStatePlugin.kt index b87d7c39..b90fd1ae 100644 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SavedStatePlugin.kt +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SavedStatePlugin.kt @@ -29,7 +29,6 @@ import pro.respawn.flowmvi.savedstate.util.EmptyBehaviorsMessage import pro.respawn.flowmvi.savedstate.util.PluginNameSuffix import pro.respawn.flowmvi.savedstate.util.restoreCatching import pro.respawn.flowmvi.savedstate.util.saveCatching -import pro.respawn.flowmvi.util.nameByType import kotlin.coroutines.CoroutineContext /** @@ -62,7 +61,7 @@ import kotlin.coroutines.CoroutineContext public fun saveStatePlugin( saver: Saver, context: CoroutineContext, - name: String? = null, + name: String? = PluginNameSuffix, behaviors: Set = SaveBehavior.Default, resetOnException: Boolean = true, ): StorePlugin = plugin { @@ -119,6 +118,6 @@ public inline fun StoreBuil saver: Saver, context: CoroutineContext, behaviors: Set = SaveBehavior.Default, - name: String? = "${this.name ?: nameByType().orEmpty()}$PluginNameSuffix", + name: String? = "${this.name.orEmpty()}$PluginNameSuffix", resetOnException: Boolean = true, ): Unit = install(saveStatePlugin(saver, context, name, behaviors, resetOnException)) diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt index 61f76768..4a991306 100644 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt @@ -1,6 +1,7 @@ package pro.respawn.flowmvi.savedstate.plugins import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.json.Json import pro.respawn.flowmvi.api.FlowMVIDSL @@ -14,30 +15,32 @@ import pro.respawn.flowmvi.savedstate.api.ThrowRecover import pro.respawn.flowmvi.savedstate.dsl.CompressedFileSaver import pro.respawn.flowmvi.savedstate.dsl.JsonSaver import pro.respawn.flowmvi.savedstate.dsl.TypedSaver +import pro.respawn.flowmvi.savedstate.util.DefaultJson import pro.respawn.flowmvi.savedstate.util.PluginNameSuffix -import pro.respawn.flowmvi.util.nameByType import kotlin.coroutines.CoroutineContext /** * An overload of [saveStatePlugin] that is configured with some default values for convenience. * - * This overload will save a GZip-compressed JSON of the state value of type [T] to a file - * in the [dir] directory and named [filename] with a specified [fileExtension]. + * This overload will save a GZip-compressed JSON (if supported by the platform) of the state value of type [T] to a + * platform-dependent place. For example, on native platforms, a File specified by [path]. + * On browser platforms, to a local storage. * * * This will save the state according to the [behaviors] specified in [SaveBehavior.Default]. * * By default, this will use [Dispatchers.Default] to save the state ([context]). * * This will only compress the JSON if the platform permits it (Android, JVM). ([CompressedFileSaver]). * * This will reset the state on exceptions in the store ([resetOnException]). * * This will invoke [recover] if an exception is encountered when saving or restoring the state. + * * By default this will throw if the state cannot be read or saved ([recover]). */ +@OptIn(ExperimentalSerializationApi::class) @FlowMVIDSL public inline fun serializeStatePlugin( - dir: String, - json: Json, + path: String, serializer: KSerializer, + json: Json = DefaultJson, behaviors: Set = SaveBehavior.Default, - filename: String = nameByType() ?: "State", - fileExtension: String = ".json", + name: String? = serializer.descriptor.serialName.plus(PluginNameSuffix), context: CoroutineContext = Dispatchers.Default, resetOnException: Boolean = true, noinline recover: suspend (Exception) -> T? = ThrowRecover, @@ -46,13 +49,13 @@ public inline fun IDE conflict @FlowMVIDSL public inline fun < @@ -71,24 +75,23 @@ public inline fun < I : MVIIntent, A : MVIAction > StoreBuilder.serializeState( - dir: String, - json: Json, + path: String, serializer: KSerializer, + json: Json = DefaultJson, + name: String? = serializer.descriptor.serialName.plus(PluginNameSuffix), behaviors: Set = SaveBehavior.Default, - fileExtension: String = ".json", context: CoroutineContext = Dispatchers.Default, resetOnException: Boolean = true, noinline recover: suspend (Exception) -> T? = ThrowRecover, ): Unit = install( serializeStatePlugin( - dir = dir, + path = path, json = json, - filename = nameByType() ?: "State", + name = name, context = context, behaviors = behaviors, resetOnException = resetOnException, recover = recover, serializer = serializer, - fileExtension = fileExtension, ) ) diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt index d7b7628f..02cde423 100644 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt @@ -1,6 +1,8 @@ package pro.respawn.flowmvi.savedstate.util import kotlinx.coroutines.CancellationException +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json import pro.respawn.flowmvi.savedstate.api.Saver @PublishedApi @@ -30,3 +32,13 @@ internal suspend fun Saver.restoreCatching(): S? = try { } catch (expected: Exception) { recover(expected) } + +@PublishedApi +@OptIn(ExperimentalSerializationApi::class) +internal val DefaultJson: Json = Json { + decodeEnumsCaseInsensitive = true + explicitNulls = false + coerceInputValues = true + allowTrailingComma = true + useAlternativeNames = true +} diff --git a/savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.js.kt b/savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.js.kt deleted file mode 100644 index 76ed4510..00000000 --- a/savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.js.kt +++ /dev/null @@ -1,6 +0,0 @@ -package pro.respawn.flowmvi.savedstate.dsl - -import kotlinx.io.files.Path - -internal actual suspend fun writeCompressed(data: String, to: Path) = write(data, to) -internal actual suspend fun readCompressed(from: Path): String? = read(from) diff --git a/savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.js.kt b/savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.js.kt new file mode 100644 index 00000000..2c8a7c57 --- /dev/null +++ b/savedstate/src/jsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.js.kt @@ -0,0 +1,16 @@ +package pro.respawn.flowmvi.savedstate.platform + +import kotlinx.browser.localStorage + +@PublishedApi +internal actual object FileAccess { + + actual suspend fun writeCompressed(data: String?, path: String) = write(data, path) + + actual suspend fun readCompressed(path: String): String? = read(path) + + actual suspend fun write(data: String?, path: String) = + if (data != null) localStorage.setItem(path, data) else localStorage.removeItem(path) + + actual suspend fun read(path: String): String? = localStorage.getItem(path) +} diff --git a/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.jvm.kt b/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.jvm.kt deleted file mode 100644 index 06149333..00000000 --- a/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.jvm.kt +++ /dev/null @@ -1,21 +0,0 @@ -package pro.respawn.flowmvi.savedstate.dsl - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.io.files.Path -import java.io.FileInputStream -import java.io.FileOutputStream -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream - -// TODO: Can't share sources between jvm and android yet - -@Suppress("INVISIBLE_MEMBER") // we want to access the JVM api of kotlinx.io... -internal actual suspend fun writeCompressed(data: String, to: Path) = withContext(Dispatchers.IO) { - GZIPOutputStream(FileOutputStream(to.file)).writer().use { it.write(data) } -} - -@Suppress("INVISIBLE_MEMBER") // we want to access the JVM api of kotlinx.io... -internal actual suspend fun readCompressed(from: Path): String? = withContext(Dispatchers.IO) { - GZIPInputStream(FileInputStream(from.file)).reader().use { it.readText() } -} diff --git a/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt b/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt new file mode 100644 index 00000000..e066e353 --- /dev/null +++ b/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt @@ -0,0 +1,50 @@ +package pro.respawn.flowmvi.savedstate.platform + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.InputStream +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +@PublishedApi +internal actual object FileAccess { + + private fun File.outputStreamOrEmpty() = run { + mkdirs() + createNewFile() + outputStream() + } + + private fun InputStream.readOrNull() = reader().use { it.readText() }.takeIf { it.isNotBlank() } + + actual suspend fun writeCompressed(data: String?, path: String) = withContext(Dispatchers.IO) { + val file = File(path) + if (data == null) { + file.delete() + return@withContext + } + file.outputStreamOrEmpty().let(::GZIPOutputStream).writer().use { it.write(data) } + } + + actual suspend fun readCompressed(path: String): String? = withContext(Dispatchers.IO) { + val file = File(path) + if (!file.exists()) return@withContext null + file.inputStream().let(::GZIPInputStream).readOrNull() + } + + actual suspend fun write(data: String?, path: String) = withContext(Dispatchers.IO) { + val file = File(path) + if (data == null) { + file.delete() + return@withContext + } + file.outputStreamOrEmpty().writer().use { it.write(data) } + } + + actual suspend fun read(path: String): String? = withContext(Dispatchers.IO) { + val file = File(path) + if (!file.exists()) return@withContext null + file.inputStream().readOrNull() + } +} diff --git a/savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.native.kt b/savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.native.kt deleted file mode 100644 index 76ed4510..00000000 --- a/savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.native.kt +++ /dev/null @@ -1,6 +0,0 @@ -package pro.respawn.flowmvi.savedstate.dsl - -import kotlinx.io.files.Path - -internal actual suspend fun writeCompressed(data: String, to: Path) = write(data, to) -internal actual suspend fun readCompressed(from: Path): String? = read(from) diff --git a/savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.native.kt b/savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.native.kt new file mode 100644 index 00000000..f589d2fe --- /dev/null +++ b/savedstate/src/nativeMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.native.kt @@ -0,0 +1,34 @@ +package pro.respawn.flowmvi.savedstate.platform + +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.readString +import kotlinx.io.writeString + +@PublishedApi +internal actual object FileAccess { + + actual suspend fun writeCompressed(data: String?, path: String) = write(data, path) + + actual suspend fun readCompressed(path: String): String? = read(path) + + @OptIn(ExperimentalStdlibApi::class) + actual suspend fun write(data: String?, path: String) { + SystemFileSystem.run { + val file = Path(path) + if (data == null) { + delete(file, false) + return@run + } + sink(file).buffered().use { it.writeString(data) } + } + } + + @OptIn(ExperimentalStdlibApi::class) + actual suspend fun read(path: String): String? = SystemFileSystem + .source(Path(path)) + .buffered() + .use { it.readString() } + .takeIf { it.isNotBlank() } +} diff --git a/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/DefaultFileSaver.kt b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/DefaultFileSaver.kt new file mode 100644 index 00000000..6424b93c --- /dev/null +++ b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/DefaultFileSaver.kt @@ -0,0 +1,76 @@ +package pro.respawn.flowmvi.savedstate.dsl + +import kotlinx.io.files.Path +import pro.respawn.flowmvi.savedstate.api.Saver +import pro.respawn.flowmvi.savedstate.api.ThrowRecover +import pro.respawn.flowmvi.savedstate.platform.FileAccess + +/** + * A [Saver] implementation that saves the given state to a file in a specified [dir] and [fileName]. + * + * * You still need to provide your own [write] and [read] functions for this overload. + * Use [FileSaver] and [CompressedFileSaver] if you want to save already serialized state. + * + * * This saver creates the necessary [dir] and file if not present and writes to file in an atomic way using a [Mutex]. + * * If `null` is passed to [Saver.save], it will delete the file, but not the directory. + * * The writes to the file cannot be canceled to prevent saving partial data. + */ +public inline fun DefaultFileSaver( + dir: String, + fileName: String, + crossinline write: suspend (data: T?, to: Path) -> Unit, + crossinline read: suspend (from: Path) -> T?, + crossinline recover: suspend (Exception) -> T?, +): Saver = DefaultFileSaver( + path = Path(dir, fileName).name, + write = { data: T?, path: String -> write(data, Path(path)) }, + read = { path -> read(Path(path)) }, + recover = recover, +) + +/** + * A [DefaultFileSaver] implementation that saves [String] state to the file system. + * + * Usually used as a decorator for [JsonSaver] + * + * See the overload for more details. + * @see DefaultFileSaver + * @see JsonSaver + * @see Saver + */ +public fun FileSaver( + dir: String, + fileName: String, + recover: suspend (Exception) -> String? = ThrowRecover, +): Saver = DefaultFileSaver( + dir = dir, + fileName = fileName, + recover = recover, + write = { data, path -> FileAccess.write(data, path.name) }, + read = { path -> FileAccess.read(path.name) }, +) + +/** + * A [DefaultFileSaver] implementation that saves a **compressed** [String] state to the file system. + * Usually used as a decorator for [JsonSaver] + * + * This saver is only available on JVM and Android for now, and therefore will be identical to [FileSaver] + * on other platforms. (will **not** compress the file) + * + * See the overload for more details. + * + * @see DefaultFileSaver + * @see JsonSaver + * @see Saver + */ +public fun CompressedFileSaver( + dir: String, + fileName: String, + recover: suspend (Exception) -> String? = ThrowRecover, +): Saver = DefaultFileSaver( + dir = dir, + fileName = fileName, + recover = recover, + write = { data, path -> FileAccess.writeCompressed(data, path.name) }, + read = { path -> FileAccess.readCompressed(path.name) }, +) diff --git a/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt new file mode 100644 index 00000000..44cdf2ea --- /dev/null +++ b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt @@ -0,0 +1,100 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package pro.respawn.flowmvi.savedstate.plugins + +import kotlinx.coroutines.Dispatchers +import kotlinx.io.files.Path +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import pro.respawn.flowmvi.api.FlowMVIDSL +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.api.StorePlugin +import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.savedstate.api.SaveBehavior +import pro.respawn.flowmvi.savedstate.api.ThrowRecover +import pro.respawn.flowmvi.savedstate.dsl.CompressedFileSaver +import pro.respawn.flowmvi.savedstate.dsl.JsonSaver +import pro.respawn.flowmvi.savedstate.dsl.TypedSaver +import pro.respawn.flowmvi.savedstate.util.PluginNameSuffix +import kotlin.coroutines.CoroutineContext + +/** + * An overload of [saveStatePlugin] that is configured with some default values for convenience. + * + * This overload will save a GZip-compressed JSON of the state value of type [T] to a file + * in the [dir] directory and named [filename] with a specified [fileExtension]. + * + * * This will save the state according to the [behaviors] specified in [SaveBehavior.Default]. + * * By default, this will use [Dispatchers.Default] to save the state ([context]). + * * This will only compress the JSON if the platform permits it (Android, JVM). ([CompressedFileSaver]). + * * This will reset the state on exceptions in the store ([resetOnException]). + * * This will invoke [recover] if an exception is encountered when saving or restoring the state. + */ +@Deprecated("Please use an overload that explicitly provides a full path") +@FlowMVIDSL +public inline fun serializeStatePlugin( + dir: String, + json: Json, + serializer: KSerializer, + behaviors: Set = SaveBehavior.Default, + filename: String = serializer.descriptor.serialName, + fileExtension: String = ".json", + context: CoroutineContext = Dispatchers.Default, + resetOnException: Boolean = true, + noinline recover: suspend (Exception) -> T? = ThrowRecover, +): StorePlugin = saveStatePlugin( + saver = TypedSaver( + JsonSaver( + json = json, + serializer = serializer, + delegate = CompressedFileSaver(Path(dir, "$filename$fileExtension").name, ThrowRecover), + recover = recover + ) + ), + behaviors = behaviors, + context = context, + name = "$filename$PluginNameSuffix", + resetOnException = resetOnException +) + +/** + * Install a [serializeStatePlugin]. + * + * Please see the parent overload for more info. + * + * @see serializeStatePlugin + */ +@Suppress("Indentation") // detekt <> IDE conflict +@FlowMVIDSL +@Deprecated("Please use an overload that explicitly provides a full path") +public inline fun < + reified T : S, + reified S : MVIState, + I : MVIIntent, + A : MVIAction + > StoreBuilder.serializeState( + dir: String, + json: Json, + serializer: KSerializer, + behaviors: Set = SaveBehavior.Default, + fileExtension: String = ".json", + filename: String = serializer.descriptor.serialName, + context: CoroutineContext = Dispatchers.Default, + resetOnException: Boolean = true, + noinline recover: suspend (Exception) -> T? = ThrowRecover, +): Unit = install( + serializeStatePlugin( + dir = dir, + json = json, + filename = filename, + context = context, + behaviors = behaviors, + resetOnException = resetOnException, + recover = recover, + serializer = serializer, + fileExtension = fileExtension, + ) +) diff --git a/savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.wasmJs.kt b/savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.wasmJs.kt deleted file mode 100644 index cf31648b..00000000 --- a/savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/Compress.wasmJs.kt +++ /dev/null @@ -1,8 +0,0 @@ -package pro.respawn.flowmvi.savedstate.dsl - -import kotlinx.browser.localStorage -import kotlinx.io.files.Path - -internal actual suspend fun writeCompressed(data: String, to: Path) = localStorage.setItem(to.name, data) - -internal actual suspend fun readCompressed(from: Path): String? = localStorage.getItem(from.name) diff --git a/savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.wasmJs.kt b/savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.wasmJs.kt new file mode 100644 index 00000000..2c8a7c57 --- /dev/null +++ b/savedstate/src/wasmJsMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.wasmJs.kt @@ -0,0 +1,16 @@ +package pro.respawn.flowmvi.savedstate.platform + +import kotlinx.browser.localStorage + +@PublishedApi +internal actual object FileAccess { + + actual suspend fun writeCompressed(data: String?, path: String) = write(data, path) + + actual suspend fun readCompressed(path: String): String? = read(path) + + actual suspend fun write(data: String?, path: String) = + if (data != null) localStorage.setItem(path, data) else localStorage.removeItem(path) + + actual suspend fun read(path: String): String? = localStorage.getItem(path) +} From cc634ade44ce89059f812711865bb0c2d6d914f1 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 17:40:42 +0300 Subject: [PATCH 04/15] migrate sample app to new savedState API --- .../sample/platform/AndroidFileManager.kt | 2 +- .../configuration/DefaultStoreConfiguration.kt | 16 +++++----------- .../features/savedstate/SavedStateContainer.kt | 8 +++----- .../features/savedstate/SavedStateScreen.kt | 4 +--- .../flowmvi/sample/platform/FileManager.kt | 3 ++- .../pro/respawn/flowmvi/sample/util/Compose.kt | 2 +- .../flowmvi/sample/platform/JvmFileManager.kt | 6 ++---- .../sample/platform/BrowserFileManager.kt | 2 +- savedstate/build.gradle.kts | 7 +++++++ .../savedstate/platform/FileAccess.android.kt | 2 +- .../savedstate/platform/FileAccess.jvm.kt | 2 +- 11 files changed, 25 insertions(+), 29 deletions(-) diff --git a/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/platform/AndroidFileManager.kt b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/platform/AndroidFileManager.kt index 62a9004b..044c43b9 100644 --- a/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/platform/AndroidFileManager.kt +++ b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/platform/AndroidFileManager.kt @@ -6,5 +6,5 @@ class AndroidFileManager(context: Context) : FileManager { private val cacheDir = context.cacheDir - override fun cacheDir(relative: String): String = cacheDir.resolve(relative).absolutePath + override fun cacheFile(dir: String, filename: String): String = cacheDir.resolve(dir).resolve(filename).absolutePath } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt index 5d5e2a7e..205844de 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt @@ -17,26 +17,20 @@ import pro.respawn.flowmvi.savedstate.api.NullRecover import pro.respawn.flowmvi.savedstate.api.Saver import pro.respawn.flowmvi.savedstate.dsl.CompressedFileSaver import pro.respawn.flowmvi.savedstate.dsl.JsonSaver -import pro.respawn.flowmvi.savedstate.dsl.NoOpSaver import pro.respawn.flowmvi.savedstate.plugins.saveStatePlugin internal class DefaultStoreConfiguration( - files: FileManager, + private val files: FileManager, private val json: Json, ) : StoreConfiguration { - private val cacheDir = files.cacheDir(StoreCacheDirName) - override fun saver( serializer: KSerializer, fileName: String, - ): Saver { - return CompressedFileSaver( - dir = cacheDir ?: return NoOpSaver(), - fileName = "$fileName.gz", - recover = NullRecover - ).let { JsonSaver(json, serializer, it) } - } + ) = CompressedFileSaver( + path = files.cacheFile("states", "$fileName.json"), + recover = NullRecover + ).let { JsonSaver(json, serializer, it) } override operator fun StoreBuilder.invoke( name: String, diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt index 79993451..8d42d1a0 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt @@ -1,6 +1,5 @@ package pro.respawn.flowmvi.sample.features.savedstate -import kotlinx.serialization.json.Json import pro.respawn.flowmvi.api.Container import pro.respawn.flowmvi.dsl.store import pro.respawn.flowmvi.dsl.useState @@ -11,6 +10,7 @@ import pro.respawn.flowmvi.sample.features.savedstate.SavedStateFeatureState.Dis import pro.respawn.flowmvi.sample.features.savedstate.SavedStateIntent.ChangedInput import pro.respawn.flowmvi.sample.platform.FileManager import pro.respawn.flowmvi.savedstate.api.NullRecover +import pro.respawn.flowmvi.savedstate.api.ThrowRecover import pro.respawn.flowmvi.savedstate.plugins.serializeState import pro.respawn.kmmutils.inputforms.dsl.input import pro.respawn.flowmvi.sample.features.savedstate.SavedStateFeatureState as State @@ -19,7 +19,6 @@ import pro.respawn.flowmvi.sample.features.savedstate.SavedStateIntent as Intent internal class SavedStateContainer( configuration: StoreConfiguration, fileManager: FileManager, - json: Json, ) : Container { override val store = store(DisplayingInput()) { @@ -28,10 +27,9 @@ internal class SavedStateContainer( // can also be injected, defined here for illustration purposes // see "StoreConfiguration" for injection setup serializeState( - dir = fileManager.cacheDir(name!!), - json = json, + path = fileManager.cacheFile("saved_state", "state"), serializer = DisplayingInput.serializer(), - recover = NullRecover, + recover = ThrowRecover, ) reduce { intent -> diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt index 4b8eec6b..52a36a85 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt @@ -52,14 +52,12 @@ private const val Description = """ private const val Code = """ internal class SavedStateContainer( fileManager: FileManager, - json: Json, ) : Container { override val store = store(DisplayingInput()) { serializeState( dir = fileManager.cacheDir("state"), - json = json, serializer = DisplayingInput.serializer(), ) @@ -118,7 +116,7 @@ private fun IntentReceiver.SavedStateScreenContent( ) Spacer(Modifier.height(24.dp)) @Suppress("MagicNumber") - CodeText(Code, PhraseLocation(183, 338)) + CodeText(Code, PhraseLocation(167, 297)) } } } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/platform/FileManager.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/platform/FileManager.kt index 49b5651b..faa31236 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/platform/FileManager.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/platform/FileManager.kt @@ -1,5 +1,6 @@ package pro.respawn.flowmvi.sample.platform interface FileManager { - fun cacheDir(relative: String): String + + fun cacheFile(dir: String, filename: String): String } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/util/Compose.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/util/Compose.kt index 59f4d469..4206d64e 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/util/Compose.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/util/Compose.kt @@ -162,7 +162,7 @@ fun Modifier.fadingEdge( @Composable fun String.branded(color: Color = MaterialTheme.colorScheme.primary) = buildAnnotatedString { when { - !isValid -> return@buildAnnotatedString + !isValid() -> return@buildAnnotatedString !first().isLetterOrDigit() -> return AnnotatedString(this@branded) else -> { withStyle(style = SpanStyle(color = color)) { append(first()) } diff --git a/sample/src/desktopMain/kotlin/pro/respawn/flowmvi/sample/platform/JvmFileManager.kt b/sample/src/desktopMain/kotlin/pro/respawn/flowmvi/sample/platform/JvmFileManager.kt index 1bd14ece..85e901f7 100644 --- a/sample/src/desktopMain/kotlin/pro/respawn/flowmvi/sample/platform/JvmFileManager.kt +++ b/sample/src/desktopMain/kotlin/pro/respawn/flowmvi/sample/platform/JvmFileManager.kt @@ -4,9 +4,7 @@ import java.io.File class JvmFileManager : FileManager { - private val cacheDir by lazy { - File("cache").apply { mkdirs() } - } + private val cacheDir by lazy { File(".cache").apply { mkdirs() } } - override fun cacheDir(relative: String): String = cacheDir.resolve(relative).absolutePath + override fun cacheFile(dir: String, filename: String): String = cacheDir.resolve(dir).resolve(filename).absolutePath } diff --git a/sample/src/wasmJsMain/kotlin/pro/respawn/flowmvi/sample/platform/BrowserFileManager.kt b/sample/src/wasmJsMain/kotlin/pro/respawn/flowmvi/sample/platform/BrowserFileManager.kt index fde9ac76..3ad5996b 100644 --- a/sample/src/wasmJsMain/kotlin/pro/respawn/flowmvi/sample/platform/BrowserFileManager.kt +++ b/sample/src/wasmJsMain/kotlin/pro/respawn/flowmvi/sample/platform/BrowserFileManager.kt @@ -2,5 +2,5 @@ package pro.respawn.flowmvi.sample.platform internal class BrowserFileManager : FileManager { - override fun cacheDir(relative: String): String = "cache/$relative" + override fun cacheFile(dir: String, filename: String): String = "cache/$dir/$filename" } diff --git a/savedstate/build.gradle.kts b/savedstate/build.gradle.kts index af76ade5..84324f30 100644 --- a/savedstate/build.gradle.kts +++ b/savedstate/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + plugins { kotlin("multiplatform") alias(libs.plugins.serialization) @@ -7,6 +9,7 @@ plugins { } kotlin { + @OptIn(ExperimentalKotlinGradlePluginApi::class) configureMultiplatform(this) { common { group("nonBrowser") { @@ -22,6 +25,7 @@ kotlin { } sourceSets { + val nonBrowserMain by getting nativeMain.dependencies { implementation(libs.kotlin.io) } @@ -33,6 +37,9 @@ kotlin { api(libs.kotlin.serialization.json) implementation(libs.kotlin.atomicfu) } + nonBrowserMain.dependencies { + implementation(libs.kotlin.io) + } } } diff --git a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt index 6725a643..68f6e19f 100644 --- a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt +++ b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt @@ -12,7 +12,7 @@ import java.util.zip.GZIPOutputStream internal actual object FileAccess { private fun File.outputStreamOrEmpty() = run { - mkdirs() + parentFile?.mkdirs() createNewFile() outputStream() } diff --git a/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt b/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt index e066e353..efe6c8ae 100644 --- a/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt +++ b/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt @@ -11,7 +11,7 @@ import java.util.zip.GZIPOutputStream internal actual object FileAccess { private fun File.outputStreamOrEmpty() = run { - mkdirs() + parentFile?.mkdirs() createNewFile() outputStream() } From d1d2901e98b6b6739c32b7337c08da37392015b8 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 17:57:36 +0300 Subject: [PATCH 05/15] buffer saved data when writing to file --- .../flowmvi/savedstate/platform/FileAccess.android.kt | 7 +++---- .../respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt index 68f6e19f..c29a9582 100644 --- a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt +++ b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.android.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.InputStream -import java.io.InputStreamReader import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream @@ -17,7 +16,7 @@ internal actual object FileAccess { outputStream() } - private fun InputStream.readOrNull() = reader().use { it.readText() }.takeIf { it.isNotBlank() } + private fun InputStream.readOrNull() = bufferedReader().use { it.readText() }.takeIf { it.isNotBlank() } actual suspend fun writeCompressed(data: String?, path: String) = withContext(Dispatchers.IO) { val file = File(path) @@ -25,7 +24,7 @@ internal actual object FileAccess { file.delete() return@withContext } - file.outputStreamOrEmpty().let(::GZIPOutputStream).writer().use { it.write(data) } + file.outputStreamOrEmpty().let(::GZIPOutputStream).bufferedWriter().use { it.write(data) } } actual suspend fun readCompressed(path: String): String? = withContext(Dispatchers.IO) { @@ -40,7 +39,7 @@ internal actual object FileAccess { file.delete() return@withContext } - file.outputStreamOrEmpty().writer().use { it.write(data) } + file.outputStreamOrEmpty().bufferedWriter().use { it.write(data) } } actual suspend fun read(path: String): String? = withContext(Dispatchers.IO) { diff --git a/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt b/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt index efe6c8ae..c29a9582 100644 --- a/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt +++ b/savedstate/src/jvmMain/kotlin/pro/respawn/flowmvi/savedstate/platform/FileAccess.jvm.kt @@ -16,7 +16,7 @@ internal actual object FileAccess { outputStream() } - private fun InputStream.readOrNull() = reader().use { it.readText() }.takeIf { it.isNotBlank() } + private fun InputStream.readOrNull() = bufferedReader().use { it.readText() }.takeIf { it.isNotBlank() } actual suspend fun writeCompressed(data: String?, path: String) = withContext(Dispatchers.IO) { val file = File(path) @@ -24,7 +24,7 @@ internal actual object FileAccess { file.delete() return@withContext } - file.outputStreamOrEmpty().let(::GZIPOutputStream).writer().use { it.write(data) } + file.outputStreamOrEmpty().let(::GZIPOutputStream).bufferedWriter().use { it.write(data) } } actual suspend fun readCompressed(path: String): String? = withContext(Dispatchers.IO) { @@ -39,7 +39,7 @@ internal actual object FileAccess { file.delete() return@withContext } - file.outputStreamOrEmpty().writer().use { it.write(data) } + file.outputStreamOrEmpty().bufferedWriter().use { it.write(data) } } actual suspend fun read(path: String): String? = withContext(Dispatchers.IO) { From fe28f8cb30d9ab31afcc8cb27c4146335ef033b8 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 17:58:21 +0300 Subject: [PATCH 06/15] added LoggingSaver --- .../flowmvi/savedstate/dsl/LoggingSaver.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/LoggingSaver.kt diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/LoggingSaver.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/LoggingSaver.kt new file mode 100644 index 00000000..088fd18a --- /dev/null +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/LoggingSaver.kt @@ -0,0 +1,22 @@ +package pro.respawn.flowmvi.savedstate.dsl + +import pro.respawn.flowmvi.logging.PlatformStoreLogger +import pro.respawn.flowmvi.logging.StoreLogLevel +import pro.respawn.flowmvi.logging.StoreLogger +import pro.respawn.flowmvi.logging.invoke +import pro.respawn.flowmvi.savedstate.api.Saver + +/** + * A [Saver] that writes to [logger] during save restoration, saving and errors. + */ +public fun LoggingSaver( + delegate: Saver, + logger: StoreLogger = PlatformStoreLogger, + level: StoreLogLevel? = null, + tag: String? = "Saver", +): Saver = CallbackSaver( + delegate, + onSave = { logger(level ?: StoreLogLevel.Trace, tag) { "Saving state: $it" } }, + onRestore = { logger(level ?: StoreLogLevel.Trace, tag) { "Restored state: $it" } }, + onException = { logger.invoke(level ?: StoreLogLevel.Error, tag, e = it) }, +) From 3efdd09f8c8a317e8144c8469edc2af089cce566 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 18:05:53 +0300 Subject: [PATCH 07/15] add logging to default serializeState plugin --- .../plugins/SerializeStatePlugin.kt | 29 ++++++++------- .../SerializeStatePlugin.nonBrowser.kt | 37 +++++++++++-------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt index 4a991306..3323ce02 100644 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt @@ -10,10 +10,13 @@ import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.StorePlugin import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.logging.PlatformStoreLogger +import pro.respawn.flowmvi.logging.StoreLogger import pro.respawn.flowmvi.savedstate.api.SaveBehavior import pro.respawn.flowmvi.savedstate.api.ThrowRecover import pro.respawn.flowmvi.savedstate.dsl.CompressedFileSaver import pro.respawn.flowmvi.savedstate.dsl.JsonSaver +import pro.respawn.flowmvi.savedstate.dsl.LoggingSaver import pro.respawn.flowmvi.savedstate.dsl.TypedSaver import pro.respawn.flowmvi.savedstate.util.DefaultJson import pro.respawn.flowmvi.savedstate.util.PluginNameSuffix @@ -42,17 +45,18 @@ public inline fun = SaveBehavior.Default, name: String? = serializer.descriptor.serialName.plus(PluginNameSuffix), context: CoroutineContext = Dispatchers.Default, + logger: StoreLogger = PlatformStoreLogger, resetOnException: Boolean = true, noinline recover: suspend (Exception) -> T? = ThrowRecover, ): StorePlugin = saveStatePlugin( - saver = TypedSaver( - JsonSaver( - json = json, - serializer = serializer, - delegate = CompressedFileSaver(path, ThrowRecover), - recover = recover - ) - ), + saver = JsonSaver( + json = json, + serializer = serializer, + delegate = CompressedFileSaver(path, ThrowRecover), + recover = recover + ) + .let { TypedSaver(it) } + .let { LoggingSaver(it, logger) }, behaviors = behaviors, context = context, name = name, @@ -78,20 +82,19 @@ public inline fun < path: String, serializer: KSerializer, json: Json = DefaultJson, - name: String? = serializer.descriptor.serialName.plus(PluginNameSuffix), + name: String? = "${this.name ?: serializer.descriptor.serialName}$PluginNameSuffix", behaviors: Set = SaveBehavior.Default, context: CoroutineContext = Dispatchers.Default, resetOnException: Boolean = true, noinline recover: suspend (Exception) -> T? = ThrowRecover, -): Unit = install( - serializeStatePlugin( +): Unit = serializeStatePlugin( path = path, json = json, name = name, context = context, behaviors = behaviors, + logger = this.logger, resetOnException = resetOnException, recover = recover, serializer = serializer, - ) -) +).let(::install) diff --git a/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt index 44cdf2ea..ccc6d9e0 100644 --- a/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt +++ b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt @@ -13,11 +13,15 @@ import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.StorePlugin import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.logging.PlatformStoreLogger +import pro.respawn.flowmvi.logging.StoreLogger import pro.respawn.flowmvi.savedstate.api.SaveBehavior import pro.respawn.flowmvi.savedstate.api.ThrowRecover import pro.respawn.flowmvi.savedstate.dsl.CompressedFileSaver import pro.respawn.flowmvi.savedstate.dsl.JsonSaver +import pro.respawn.flowmvi.savedstate.dsl.LoggingSaver import pro.respawn.flowmvi.savedstate.dsl.TypedSaver +import pro.respawn.flowmvi.savedstate.util.DefaultJson import pro.respawn.flowmvi.savedstate.util.PluginNameSuffix import kotlin.coroutines.CoroutineContext @@ -37,26 +41,28 @@ import kotlin.coroutines.CoroutineContext @FlowMVIDSL public inline fun serializeStatePlugin( dir: String, - json: Json, serializer: KSerializer, + json: Json = DefaultJson, behaviors: Set = SaveBehavior.Default, filename: String = serializer.descriptor.serialName, fileExtension: String = ".json", context: CoroutineContext = Dispatchers.Default, + logger: StoreLogger = PlatformStoreLogger, resetOnException: Boolean = true, + name: String? = "$filename$PluginNameSuffix", noinline recover: suspend (Exception) -> T? = ThrowRecover, ): StorePlugin = saveStatePlugin( - saver = TypedSaver( - JsonSaver( - json = json, - serializer = serializer, - delegate = CompressedFileSaver(Path(dir, "$filename$fileExtension").name, ThrowRecover), - recover = recover - ) - ), + saver = JsonSaver( + json = json, + serializer = serializer, + delegate = CompressedFileSaver(Path(dir, "$filename$fileExtension").name, ThrowRecover), + recover = recover + ) + .let { TypedSaver(it) } + .let { LoggingSaver(it, logger) }, behaviors = behaviors, context = context, - name = "$filename$PluginNameSuffix", + name = name, resetOnException = resetOnException ) @@ -77,16 +83,16 @@ public inline fun < A : MVIAction > StoreBuilder.serializeState( dir: String, - json: Json, + json: Json = DefaultJson, serializer: KSerializer, behaviors: Set = SaveBehavior.Default, fileExtension: String = ".json", filename: String = serializer.descriptor.serialName, + name: String? = "${this.name ?: filename}$PluginNameSuffix", context: CoroutineContext = Dispatchers.Default, resetOnException: Boolean = true, noinline recover: suspend (Exception) -> T? = ThrowRecover, -): Unit = install( - serializeStatePlugin( +): Unit = serializeStatePlugin( dir = dir, json = json, filename = filename, @@ -96,5 +102,6 @@ public inline fun < recover = recover, serializer = serializer, fileExtension = fileExtension, - ) -) + name = name, + logger = this.logger, +).let(::install) From 8944e2c6765593506cddb894c2af6dd2b8feca6a Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 18:10:13 +0300 Subject: [PATCH 08/15] require non-anonymous object to be used as state for saveState plugins --- .../pro/respawn/flowmvi/savedstate/dsl/SavedStateSaver.kt | 2 +- .../flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/SavedStateSaver.kt b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/SavedStateSaver.kt index f9d1c698..fd3bb2e8 100644 --- a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/SavedStateSaver.kt +++ b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/dsl/SavedStateSaver.kt @@ -33,6 +33,6 @@ public fun SavedStateHandleSaver( */ public inline fun ParcelableSaver( handle: SavedStateHandle, - key: String = nameByType() ?: "State", + key: String = "${requireNotNull(nameByType())}State", noinline recover: suspend (e: Exception) -> T? = ThrowRecover, ): Saver where T : Parcelable, T : MVIState = SavedStateHandleSaver(handle, key, recover) diff --git a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt index 793eecbe..740f954c 100644 --- a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt +++ b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt @@ -36,7 +36,7 @@ import kotlin.coroutines.CoroutineContext public inline fun parcelizeStatePlugin( handle: SavedStateHandle, context: CoroutineContext = Dispatchers.IO, - key: String = nameByType() ?: "State", + key: String = "${requireNotNull(nameByType())}State", behaviors: Set = SaveBehavior.Default, resetOnException: Boolean = true, name: String = "$key$PluginNameSuffix", @@ -63,7 +63,7 @@ public inline fun StoreBuilder.parcelizeState( handle: SavedStateHandle, context: CoroutineContext = Dispatchers.IO, - key: String = "${this.name ?: nameByType().orEmpty()}State", + key: String = "${this.name ?: requireNotNull(nameByType())}State", behaviors: Set = SaveBehavior.Default, name: String = "$key$PluginNameSuffix", resetOnException: Boolean = true, From 0014ca45d1c6828bdb744103e7607f5b64e9972b Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 18:12:58 +0300 Subject: [PATCH 09/15] add logging to parcelize state plugin --- .../plugins/ParcelizeStatePlugin.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt index 740f954c..4e48f8e3 100644 --- a/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt +++ b/savedstate/src/androidMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/ParcelizeStatePlugin.kt @@ -9,8 +9,11 @@ import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.StorePlugin import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.logging.PlatformStoreLogger +import pro.respawn.flowmvi.logging.StoreLogger import pro.respawn.flowmvi.savedstate.api.SaveBehavior import pro.respawn.flowmvi.savedstate.api.ThrowRecover +import pro.respawn.flowmvi.savedstate.dsl.LoggingSaver import pro.respawn.flowmvi.savedstate.dsl.MapSaver import pro.respawn.flowmvi.savedstate.dsl.ParcelableSaver import pro.respawn.flowmvi.savedstate.dsl.TypedSaver @@ -39,10 +42,11 @@ public inline fun = SaveBehavior.Default, resetOnException: Boolean = true, - name: String = "$key$PluginNameSuffix", + logger: StoreLogger = PlatformStoreLogger, + name: String? = "$key$PluginNameSuffix", noinline recover: suspend (Exception) -> T? = ThrowRecover, ): StorePlugin where T : Parcelable, T : S = saveStatePlugin( - saver = TypedSaver(ParcelableSaver(handle, key, recover)), + saver = LoggingSaver(TypedSaver(ParcelableSaver(handle, key, recover)), logger), context = context, name = name, behaviors = behaviors, @@ -65,9 +69,19 @@ public inline fun = SaveBehavior.Default, - name: String = "$key$PluginNameSuffix", + name: String? = "$key$PluginNameSuffix", resetOnException: Boolean = true, + logger: StoreLogger = this.logger, noinline recover: suspend (Exception) -> T? = ThrowRecover, ): Unit where T : Parcelable, T : S = install( - parcelizeStatePlugin(handle, context, key, behaviors, resetOnException, name, recover) + parcelizeStatePlugin( + handle = handle, + context = context, + key = key, + behaviors = behaviors, + resetOnException = resetOnException, + logger = logger, + name = name, + recover = recover + ) ) From c61b6c0f74ce6ec2d81dc4dd5b05d77eef896c3e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 18:14:57 +0300 Subject: [PATCH 10/15] add logging saved state to sample app --- .../sample/arch/configuration/DefaultStoreConfiguration.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt index 205844de..282359b2 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/arch/configuration/DefaultStoreConfiguration.kt @@ -17,6 +17,7 @@ import pro.respawn.flowmvi.savedstate.api.NullRecover import pro.respawn.flowmvi.savedstate.api.Saver import pro.respawn.flowmvi.savedstate.dsl.CompressedFileSaver import pro.respawn.flowmvi.savedstate.dsl.JsonSaver +import pro.respawn.flowmvi.savedstate.dsl.LoggingSaver import pro.respawn.flowmvi.savedstate.plugins.saveStatePlugin internal class DefaultStoreConfiguration( @@ -47,7 +48,7 @@ internal class DefaultStoreConfiguration( } if (saver != null) install( saveStatePlugin( - saver = saver, + saver = LoggingSaver(saver, tag = name, logger = logger), name = "${name}SavedStatePlugin", context = Dispatchers.Default, ) From 2eb32d95f2a2c98812fbcb6e9a3a6dc36cbc4770 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 18:22:33 +0300 Subject: [PATCH 11/15] fixed adaptive layouts on sample app --- .../sample/features/decompose/DecomposeScreen.kt | 3 ++- .../sample/features/diconfig/DIConfigScreen.kt | 14 +++++++++++--- .../sample/features/logging/LoggingScreen.kt | 3 ++- .../sample/features/savedstate/SavedStateScreen.kt | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/decompose/DecomposeScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/decompose/DecomposeScreen.kt index b8709a0f..d0a5f9ad 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/decompose/DecomposeScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/decompose/DecomposeScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -135,7 +136,7 @@ private fun DecomposeScreenContent( } is DisplayingPages -> Column( modifier = Modifier - .fillMaxSize() + .fillMaxHeight() .adaptiveWidth() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt index cf429b97..4c17c893 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt @@ -3,6 +3,7 @@ package pro.respawn.flowmvi.sample.features.diconfig import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -11,11 +12,14 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import dev.snipme.highlights.model.PhraseLocation import org.jetbrains.compose.resources.stringResource +import pro.respawn.flowmvi.compose.dsl.requireLifecycle +import pro.respawn.flowmvi.compose.dsl.subscribe import pro.respawn.flowmvi.sample.arch.di.container import pro.respawn.flowmvi.sample.generated.resources.Res import pro.respawn.flowmvi.sample.generated.resources.di_feature_title @@ -76,17 +80,19 @@ internal class DiConfigContainer( fun DiConfigScreen( navigator: Navigator, ) = with(container()) { + val state by subscribe(requireLifecycle()) RScaffold( onBack = navigator.backNavigator, title = stringResource(Res.string.di_feature_title), ) { - DiConfigScreenContent() + DiConfigScreenContent(state) } } @Composable -private fun DiConfigScreenContent() = Column( - modifier = Modifier.fillMaxSize() +private fun DiConfigScreenContent(state: PersistedCounterState) = Column( + modifier = Modifier + .fillMaxHeight() .adaptiveWidth() .padding(horizontal = 12.dp) .verticalScroll(rememberScrollState()), @@ -95,5 +101,7 @@ private fun DiConfigScreenContent() = Column( ) { Text(Description.trimIndent()) Spacer(Modifier.height(12.dp)) + Text("Persisted counter state: ${state.counter}") + Spacer(Modifier.height(12.dp)) CodeText(Code, PhraseLocation(start = 198, end = 357)) } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt index 445fd975..e15f2bbc 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt @@ -2,6 +2,7 @@ package pro.respawn.flowmvi.sample.features.logging import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -108,7 +109,7 @@ private fun IntentReceiver.LoggingScreenContent( is LoggingState.Loading -> CircularProgressIndicator() is LoggingState.Error -> RErrorView(e) is DisplayingLogs -> LazyColumn( - modifier = Modifier.fillMaxSize().adaptiveWidth(), + modifier = Modifier.fillMaxHeight().adaptiveWidth(), state = listState, horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt index 52a36a85..5d3ae4da 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt @@ -3,6 +3,7 @@ package pro.respawn.flowmvi.sample.features.savedstate import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width @@ -93,7 +94,7 @@ private fun IntentReceiver.SavedStateScreenContent( ) = TypeCrossfade(state) { when (this) { is DisplayingInput -> Column( - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).adaptiveWidth(), + modifier = Modifier.fillMaxHeight().verticalScroll(rememberScrollState()).adaptiveWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { From e3600137b6e66a67bc2bdaf52657ff5402339243 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 19:07:19 +0300 Subject: [PATCH 12/15] implement xml activity feature --- sample/androidApp/build.gradle.kts | 1 - .../androidApp/src/main/AndroidManifest.xml | 2 +- .../flowmvi/sample/app/MainActivity.kt | 1 + .../respawn/flowmvi/sample/app/XmlActivity.kt | 5 -- sample/build.gradle.kts | 4 + sample/libs.versions.toml | 2 + .../flowmvi/sample/di/AppModule.android.kt | 7 ++ .../navigation}/AndroidFeatureLauncher.kt | 5 +- .../flowmvi/sample/ui/screens/XmlActivity.kt | 75 +++++++++++++++++++ .../androidMain/res/layout/activity_xml.xml | 68 +++++++++++++++++ sample/src/androidMain/res/values/strings.xml | 14 ++++ .../xmlactivity/XmlActivityContainer.kt | 39 ++++++++++ .../features/xmlactivity/XmlActivityModels.kt | 26 +++++++ 13 files changed, 240 insertions(+), 9 deletions(-) delete mode 100644 sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/XmlActivity.kt rename sample/{androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app => src/androidMain/kotlin/pro/respawn/flowmvi/sample/navigation}/AndroidFeatureLauncher.kt (65%) create mode 100644 sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/ui/screens/XmlActivity.kt create mode 100644 sample/src/androidMain/res/layout/activity_xml.xml create mode 100644 sample/src/androidMain/res/values/strings.xml create mode 100644 sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityContainer.kt create mode 100644 sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityModels.kt diff --git a/sample/androidApp/build.gradle.kts b/sample/androidApp/build.gradle.kts index 2454d44f..91b17480 100644 --- a/sample/androidApp/build.gradle.kts +++ b/sample/androidApp/build.gradle.kts @@ -19,7 +19,6 @@ android { buildFeatures { buildConfig = true compose = true - viewBinding = true } applicationVariants.all { setProperty("archivesBaseName", Config.Sample.namespace) diff --git a/sample/androidApp/src/main/AndroidManifest.xml b/sample/androidApp/src/main/AndroidManifest.xml index 1f7962fe..728ba2b3 100644 --- a/sample/androidApp/src/main/AndroidManifest.xml +++ b/sample/androidApp/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ diff --git a/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/MainActivity.kt b/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/MainActivity.kt index edf4085e..bdf3fe57 100644 --- a/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/MainActivity.kt +++ b/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/MainActivity.kt @@ -10,6 +10,7 @@ import org.koin.android.scope.AndroidScopeComponent import org.koin.androidx.compose.KoinAndroidContext import org.koin.androidx.scope.activityRetainedScope import org.koin.core.annotation.KoinExperimentalAPI +import pro.respawn.flowmvi.sample.navigation.AndroidFeatureLauncher import pro.respawn.flowmvi.sample.navigation.AppContent import pro.respawn.flowmvi.sample.navigation.component.RootComponent import pro.respawn.kmmutils.common.fastLazy diff --git a/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/XmlActivity.kt b/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/XmlActivity.kt deleted file mode 100644 index abb6f2de..00000000 --- a/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/XmlActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package pro.respawn.flowmvi.sample.app - -import androidx.activity.ComponentActivity - -internal class XmlActivity : ComponentActivity() diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index cd97a335..fefc8999 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -97,6 +97,9 @@ kotlin { implementation(compose.desktop.currentOs) } androidMain.dependencies { + implementation(projects.android) + implementation(applibs.view.constraintlayout) + implementation(applibs.view.material) implementation(applibs.koin.android) } wasmJsMain.dependencies { @@ -108,6 +111,7 @@ android { namespace = Config.Sample.namespace configureAndroidLibrary(this) buildFeatures { + viewBinding = true buildConfig = true compose = true } diff --git a/sample/libs.versions.toml b/sample/libs.versions.toml index 7115c43a..2888bba0 100644 --- a/sample/libs.versions.toml +++ b/sample/libs.versions.toml @@ -7,6 +7,7 @@ material = "1.12.0-rc01" activity = "1.9.0-rc01" okio = "3.9.0" splashscreen = "1.1.0-rc01" +xml-constraintlayout = "2.2.0-alpha13" #codehighlights = "0.8.0" [libraries] @@ -29,6 +30,7 @@ compose-activity = { module = "androidx.activity:activity-compose", version.ref androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } view-material = { module = "com.google.android.material:material", version.ref = "material" } +view-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "xml-constraintlayout" } #compose-codehighlighting = { module = "dev.snipme:highlights", version.ref = "codehighlights" } diff --git a/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/di/AppModule.android.kt b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/di/AppModule.android.kt index 2005e178..e8f528a0 100644 --- a/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/di/AppModule.android.kt +++ b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/di/AppModule.android.kt @@ -1,11 +1,18 @@ package pro.respawn.flowmvi.sample.di +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.qualifier import org.koin.dsl.bind import org.koin.dsl.module +import pro.respawn.flowmvi.android.StoreViewModel +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityContainer import pro.respawn.flowmvi.sample.platform.AndroidFileManager import pro.respawn.flowmvi.sample.platform.FileManager actual val platformAppModule = module { singleOf(::AndroidFileManager) bind FileManager::class + factoryOf(::XmlActivityContainer) + viewModel(qualifier()) { StoreViewModel(get()) } } diff --git a/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/AndroidFeatureLauncher.kt b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/navigation/AndroidFeatureLauncher.kt similarity index 65% rename from sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/AndroidFeatureLauncher.kt rename to sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/navigation/AndroidFeatureLauncher.kt index ada40b67..7007aa83 100644 --- a/sample/androidApp/src/main/kotlin/pro/respawn/flowmvi/sample/app/AndroidFeatureLauncher.kt +++ b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/navigation/AndroidFeatureLauncher.kt @@ -1,11 +1,12 @@ -package pro.respawn.flowmvi.sample.app +package pro.respawn.flowmvi.sample.navigation import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import pro.respawn.flowmvi.sample.platform.PlatformFeatureLauncher +import pro.respawn.flowmvi.sample.ui.screens.XmlActivity -internal class AndroidFeatureLauncher(private val context: Context) : PlatformFeatureLauncher { +class AndroidFeatureLauncher(private val context: Context) : PlatformFeatureLauncher { override fun xmlActivity() = context.startActivity( Intent(context, XmlActivity::class.java).apply { diff --git a/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/ui/screens/XmlActivity.kt b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/ui/screens/XmlActivity.kt new file mode 100644 index 00000000..d97626c9 --- /dev/null +++ b/sample/src/androidMain/kotlin/pro/respawn/flowmvi/sample/ui/screens/XmlActivity.kt @@ -0,0 +1,75 @@ +package pro.respawn.flowmvi.sample.ui.screens + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.core.view.isVisible +import com.google.android.material.snackbar.Snackbar +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.qualifier.qualifier +import pro.respawn.flowmvi.android.StoreViewModel +import pro.respawn.flowmvi.android.subscribe +import pro.respawn.flowmvi.sample.R +import pro.respawn.flowmvi.sample.databinding.ActivityXmlBinding +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityAction +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityAction.ShowIncrementedSnackbar +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityContainer +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityIntent +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityIntent.ClickedIncrementCounter +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityState +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityState.DisplayingCounter +import pro.respawn.kmmutils.common.fastLazy + +private typealias ViewModel = StoreViewModel + +internal class XmlActivity : ComponentActivity() { + + private val vm by viewModel(qualifier()) + private val binding by fastLazy { ActivityXmlBinding.inflate(layoutInflater) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + subscribe(vm.store, ::consume, ::render) + with(binding) { + btnIncrement.setOnClickListener { vm.store.intent(ClickedIncrementCounter) } + } + } + + private fun render(state: XmlActivityState) = with(binding) { + when (state) { + is DisplayingCounter -> { + tvCounter.isVisible = true + btnIncrement.isVisible = true + tvCounter.text = getString(R.string.counter_template, state.counter) + + tvError.isVisible = false + progress.isVisible = false + } + is XmlActivityState.Error -> { + tvError.isVisible = true + tvError.text = state.e?.message ?: getString(R.string.error_message) + + progress.isVisible = false + tvCounter.isVisible = false + btnIncrement.isVisible = false + } + is XmlActivityState.Loading -> { + progress.isVisible = true + + tvError.isVisible = false + tvCounter.isVisible = false + btnIncrement.isVisible = false + } + } + } + + private fun consume(action: XmlActivityAction) { + when (action) { + ShowIncrementedSnackbar -> Snackbar.make( + binding.root, + getString(R.string.incremented_counter_message), + Snackbar.LENGTH_SHORT + ).show() + } + } +} diff --git a/sample/src/androidMain/res/layout/activity_xml.xml b/sample/src/androidMain/res/layout/activity_xml.xml new file mode 100644 index 00000000..e8f16fe8 --- /dev/null +++ b/sample/src/androidMain/res/layout/activity_xml.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + diff --git a/sample/src/androidMain/res/values/strings.xml b/sample/src/androidMain/res/values/strings.xml new file mode 100644 index 00000000..f5112fda --- /dev/null +++ b/sample/src/androidMain/res/values/strings.xml @@ -0,0 +1,14 @@ + + + Increment counter + Incremented counter + + This feature showcases how you can implement multiplatform business logic and hook it up to a native UI. + In this case, this activity is implemented using XML, but the store is in the shared module. + \n\nFlowMVI provides a way to wrap your stores in ViewModels and automatically start/stop them as needed, then inject them + into your UI components. + \n\nAs needed, you can even persist the state of a store into a platform-dependent container, i.e. SavedInstanceState + \nCounter : %d + + Unknown error + diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityContainer.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityContainer.kt new file mode 100644 index 00000000..3105e335 --- /dev/null +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityContainer.kt @@ -0,0 +1,39 @@ +package pro.respawn.flowmvi.sample.features.xmlactivity + +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import pro.respawn.flowmvi.api.Container +import pro.respawn.flowmvi.dsl.store +import pro.respawn.flowmvi.dsl.updateState +import pro.respawn.flowmvi.plugins.recover +import pro.respawn.flowmvi.plugins.reduce +import pro.respawn.flowmvi.sample.arch.configuration.StoreConfiguration +import pro.respawn.flowmvi.sample.arch.configuration.configure +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityAction.ShowIncrementedSnackbar +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityIntent.ClickedIncrementCounter +import pro.respawn.flowmvi.sample.features.xmlactivity.XmlActivityState.DisplayingCounter + +internal class XmlActivityContainer( + configuration: StoreConfiguration, +) : Container { + + override val store = store(DisplayingCounter(0)) { + configure(configuration, "XmlActivityStore") + recover { + updateState { XmlActivityState.Error(it) } + null + } + reduce { intent -> + when (intent) { + is ClickedIncrementCounter -> updateState { + launch { + delay(1000L) + action(ShowIncrementedSnackbar) + updateState { DisplayingCounter(counter + 1) } + } + XmlActivityState.Loading + } + } + } + } +} diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityModels.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityModels.kt new file mode 100644 index 00000000..ba0fd6d1 --- /dev/null +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/xmlactivity/XmlActivityModels.kt @@ -0,0 +1,26 @@ +package pro.respawn.flowmvi.sample.features.xmlactivity + +import androidx.compose.runtime.Immutable +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState + +@Immutable +internal sealed interface XmlActivityState : MVIState { + + data object Loading : XmlActivityState + data class Error(val e: Exception?) : XmlActivityState + data class DisplayingCounter(val counter: Int) : XmlActivityState +} + +@Immutable +internal sealed interface XmlActivityIntent : MVIIntent { + + data object ClickedIncrementCounter : XmlActivityIntent +} + +@Immutable +internal sealed interface XmlActivityAction : MVIAction { + + data object ShowIncrementedSnackbar : XmlActivityAction +} From 42262931817f73b9697c9c8ee1abc5fd35141a5d Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 19:10:57 +0300 Subject: [PATCH 13/15] fix lint, remove notice from saved state screen --- .../features/diconfig/DIConfigScreen.kt | 1 - .../sample/features/logging/LoggingScreen.kt | 1 - .../savedstate/SavedStateContainer.kt | 1 - .../features/savedstate/SavedStateScreen.kt | 14 ---------- .../plugins/SerializeStatePlugin.kt | 16 +++++------ .../SerializeStatePlugin.nonBrowser.kt | 28 +++++++++---------- 6 files changed, 22 insertions(+), 39 deletions(-) diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt index 4c17c893..1bf8d173 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/diconfig/DIConfigScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt index e15f2bbc..1d38c3a2 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/logging/LoggingScreen.kt @@ -3,7 +3,6 @@ package pro.respawn.flowmvi.sample.features.logging import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt index 8d42d1a0..3bee3ace 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt @@ -9,7 +9,6 @@ import pro.respawn.flowmvi.sample.arch.configuration.configure import pro.respawn.flowmvi.sample.features.savedstate.SavedStateFeatureState.DisplayingInput import pro.respawn.flowmvi.sample.features.savedstate.SavedStateIntent.ChangedInput import pro.respawn.flowmvi.sample.platform.FileManager -import pro.respawn.flowmvi.savedstate.api.NullRecover import pro.respawn.flowmvi.savedstate.api.ThrowRecover import pro.respawn.flowmvi.savedstate.plugins.serializeState import pro.respawn.kmmutils.inputforms.dsl.input diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt index 5d3ae4da..1b1d9e46 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateScreen.kt @@ -4,13 +4,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -22,7 +20,6 @@ import org.jetbrains.compose.resources.stringResource import pro.respawn.flowmvi.api.IntentReceiver import pro.respawn.flowmvi.compose.dsl.requireLifecycle import pro.respawn.flowmvi.compose.dsl.subscribe -import pro.respawn.flowmvi.sample.BuildFlags import pro.respawn.flowmvi.sample.arch.di.container import pro.respawn.flowmvi.sample.features.savedstate.SavedStateFeatureState.DisplayingInput import pro.respawn.flowmvi.sample.features.savedstate.SavedStateIntent.ChangedInput @@ -34,9 +31,7 @@ import pro.respawn.flowmvi.sample.ui.widgets.CodeText import pro.respawn.flowmvi.sample.ui.widgets.RScaffold import pro.respawn.flowmvi.sample.ui.widgets.RTextInput import pro.respawn.flowmvi.sample.ui.widgets.TypeCrossfade -import pro.respawn.flowmvi.sample.util.Platform import pro.respawn.flowmvi.sample.util.adaptiveWidth -import pro.respawn.flowmvi.sample.util.platform private const val Description = """ Saved state plugin allows you to persist a state of a store into a file or other place in about 5 lines of code @@ -100,15 +95,6 @@ private fun IntentReceiver.SavedStateScreenContent( ) { Text(Description.trimIndent()) Spacer(Modifier.height(24.dp)) - if (BuildFlags.platform == Platform.Web) { - Text( - text = """ - You are on web, where persisting files to file system is not supported, - so the data will not be saved - """.trimIndent(), - color = MaterialTheme.colorScheme.error, - ) - } RTextInput( input = input, onTextChange = { intent(ChangedInput(it)) }, diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt index 3323ce02..0e02e995 100644 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.kt @@ -88,13 +88,13 @@ public inline fun < resetOnException: Boolean = true, noinline recover: suspend (Exception) -> T? = ThrowRecover, ): Unit = serializeStatePlugin( - path = path, - json = json, - name = name, - context = context, - behaviors = behaviors, + path = path, + json = json, + name = name, + context = context, + behaviors = behaviors, logger = this.logger, - resetOnException = resetOnException, - recover = recover, - serializer = serializer, + resetOnException = resetOnException, + recover = recover, + serializer = serializer, ).let(::install) diff --git a/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt index ccc6d9e0..5a60e8f7 100644 --- a/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt +++ b/savedstate/src/nonBrowserMain/kotlin/pro/respawn/flowmvi/savedstate/plugins/SerializeStatePlugin.nonBrowser.kt @@ -77,11 +77,11 @@ public inline fun StoreBuilder.serializeState( + reified T : S, + reified S : MVIState, + I : MVIIntent, + A : MVIAction + > StoreBuilder.serializeState( dir: String, json: Json = DefaultJson, serializer: KSerializer, @@ -93,15 +93,15 @@ public inline fun < resetOnException: Boolean = true, noinline recover: suspend (Exception) -> T? = ThrowRecover, ): Unit = serializeStatePlugin( - dir = dir, - json = json, - filename = filename, - context = context, - behaviors = behaviors, - resetOnException = resetOnException, - recover = recover, - serializer = serializer, - fileExtension = fileExtension, + dir = dir, + json = json, + filename = filename, + context = context, + behaviors = behaviors, + resetOnException = resetOnException, + recover = recover, + serializer = serializer, + fileExtension = fileExtension, name = name, logger = this.logger, ).let(::install) From 46ef8eec992aaaaf0d35bd8d6838cdf86a8375c0 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 19:11:38 +0300 Subject: [PATCH 14/15] bump version name --- buildSrc/src/main/kotlin/Config.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index ec9dd670..087594ab 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -15,11 +15,11 @@ object Config { const val artifactId = "$group.$artifact" - const val versionCode = 2 + const val versionCode = 3 const val majorRelease = 2 const val minorRelease = 5 const val patch = 0 - const val postfix = "-alpha07" // include dash (-) + const val postfix = "-alpha08" // include dash (-) const val majorVersionName = "$majorRelease.$minorRelease.$patch" const val versionName = "$majorVersionName$postfix" const val url = "https://github.com/respawn-app/FlowMVI" From 611d5bc84456712bd902ffcbba79d4388c089034 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 17 Apr 2024 19:14:20 +0300 Subject: [PATCH 15/15] publish keychain --- .gitignore | 1 - certificates/keystore.jks | Bin 0 -> 2626 bytes 2 files changed, 1 deletion(-) create mode 100644 certificates/keystore.jks diff --git a/.gitignore b/.gitignore index 3e007363..6568eb53 100644 --- a/.gitignore +++ b/.gitignore @@ -173,4 +173,3 @@ hs_err_pid* /.idea/deploymentTargetSelector.xml **/plugin_encrypted_key.pem **/plugin_rsa_private_key.pem -/certificates/keystore.jks diff --git a/certificates/keystore.jks b/certificates/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..002c56a48581e2ccb0304c9cdb08ed22d7dd8fec GIT binary patch literal 2626 zcma);X*d*$8pq9;VK7WIvSnvx5XO@1WXUpFW)3={vhPf`Iz%yJ-(`#JImoePMvUw+ z*^^NANR~0yT#4wq&%ICQxu5R+@ILSJ{@(xp{rG=gG=Wtc2w+4LSk{>ZjI#ueV! zp9V4@TXF31Oh7qU`gL}!PwZP+x0G|cCeg=`T#*cLe{=K1Y06;NTk@pUxL&e_8Wv-_ z{m-L!13?V3dS!)Ep?>MbW~0Jvb_`Le)9bm%wF3O|e?REzl##hrW)NA>)I zK9G5a;JkWBr!%OHT!Gm~VIFEos&EonH)LSX!qdO36-IU~ACuoOX|N!N8600O+<@Wa z?o011=e}kOC&Djg+;@ZQ&L!MBv{CaF>g7u_{zwWZs>dEEmkaV~QxZd(oUz*(gn7&| zO;bj09a=K7Qy(`P%3oF;6BiOJ*B^xVjG)nB5Msd8YI_Bb3p8@9aH_&Dn;h@?QtP}q zw6&M<$rlm(IgIH{HQ9ftZltWaRxYkHtulDtaup+qb4vQOv-U`~P^bc{7rg1EM!%_X zf<0K`YT_<$K>%W3_VH#;^sU!BESD$arp-DhEz9GPIg>Pq4oB^R_zqq9Ey(aTgx5}(~hV+#)AA+#J#FrZBR2EznXqu?d8z{sqad0sPlS6JLl+t@N zvO?5}CQi7~KRfATN*5>(KeY#5m56R5RL2$k#Xs=5bx37tG&zhG!e$*)**9>w9H3_MLS?j^F}z zA6&^{Oo6H-3kT*B2^Z_VWBEsQRB(phsm68U07qk2r!{`ou4jbh5XIW4kA&^13g~&X{hKo^$yH=jZw~HniKn zr)}-a?DEu5nRegyy}^!{vK>bA`$+rQJ-^)Z1)k^+FQ6c#=K|fv#Nq>tezUAqELuFl zZi&QmuF2oO2FR)$=LH^;jWWFEF!_^(e-=PkQHLJ|lsmwusXdD~WD=g7mqQ&VxHmq3 zR=#BvrN8g`B49R^#xwar0E}5F)fo@tgd67pFUCTuhP&n(MQ@?ZGXpiLSjxd?(^vgc zIa&9$^;q{}ZhpbHC7R(gb!9CPv55g-6tdza)2OZ)qd>y-*9Rd1l++>2WL3Er)ygb& zN!)qrF@4{M+O&xi4PYp3eIPpHyn z#)=fw(cHh{k_D~+5n^KSW^iQ)L5u#4xfS3bE-O!8R}qCP3QCGv4|G?Ee$cO=X*o1s%NY|2hqrwkLr&mA4B|^(eicpqCPc3?MkPE5Ds@z-+^w#obv&E*%&A~|A6KHXF?li6 z-og1O=1;ufV4AT3^ZQvtj?Cpc6_D1Ll3`PmGUMx0&)?b079T48EUVA?`J=|cJ(?Yz zcLbm{_;v^!_#rPyw6#epvFmNUrpHM9DRvIUB^lpCA;eBK+(_h;1$U&v1Y?(8Yuq6Pt(*Ou5lT?nx|7v zcgefUU|N*Ob>M)9zRueSPBXo!H&> zLg}Cl(Jr!8@I6Xoh=Xo55tOjp@lJ4f$yQzXVO>kl_E>F>obq}G?TWu7IhP{2KH zYXq6%c(D@unymrvy>sV+7D>uhZ}@ZyFwm14dw&E%I~ zh}mE3G#5%lc)mM zP!7!cz*z%qGB+w74+xI8E!#fozILaFKhhfSE%#}1oWl=A9r&whmb{(?Kfb@{b4l~| zR_G6t=Mn%@ytG?dOT+8#OS0)^ypt>M4z(=4!PfPx?&i#UK7Acce$`}If_4C=Gk!|# zHMCyJI$dY2vBJ$UNA{8yCuq&}qNI^R)6LMmWM4(DU|7VP<`1iU+Qz%vN{AqNr5EUv zb7?@MC7Ck`v=A~6l`l7+w$a_V_LSI>5nWc19H=)};KJZ#;oi76JLvcrLrFb7SUEDj zDwZZ#U*xSH7UYn$f+Q%Z+_j04hDr^DW@54ybkK!X6#X*&2Iz8%wp3d5;+B)klP;m4 z2sQ0am_*2hWd7m{`rl4jhmDp^r;A}II{uuiL?DWxA{-)SY=SmJE1=nb{Z&8!m;od* zgvwO7Xz_~KJW%m8PU}ghO)c#o9dj#j;#uoUvF`i*AUM{F?B*RQ?ox&i$V_&mq3i%q J@qb0aKLNs`u1){| literal 0 HcmV?d00001