Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.compose) apply false
Expand Down
2 changes: 2 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import java.util.Properties
import kotlin.apply

plugins {
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.compose)
Expand Down Expand Up @@ -111,6 +112,7 @@ kotlin {
api(libs.koin.core)
api(libs.kotlinx.datetime)
api(libs.atomicfu)
api(libs.kotlinx.serialization.json)

implementation(compose.runtime)
implementation(compose.foundation)
Expand Down
5 changes: 5 additions & 0 deletions composeApp/src/commonMain/kotlin/DI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import org.koin.core.module.Module
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.bind
import org.koin.dsl.module
import service.kvstoreDemo.IKVStoreDemoService
import service.kvstoreDemo.KVStoreDemoService
import ui.app.AppViewInteractor
import ui.appStateExample.AppStateExampleViewInteractor
import ui.capability.CapabilityScreenViewInteractor
Expand All @@ -22,6 +24,7 @@ import ui.device.DeviceHomeViewInteractor
import ui.file.FileSystemViewInteractor
import ui.home.HomeViewInteractor
import ui.iosServices.IOSServicesScreenViewInteractor
import ui.kvstoreDemo.KVStoreDemoScreenViewInteractor
import ui.popups.PopupsScreenViewInteractor
import ui.viewStateExample.ViewStateExampleViewInteractor

Expand All @@ -47,6 +50,7 @@ fun commonModule() = module {

single { DeviceInteractor(get()) }
single { AppInteractor(get()) }
single { KVStoreDemoService(get()) } bind IKVStoreDemoService::class

factory { params -> AppViewInteractor(params[0], get(), get()) }
factory { ScreenViewInteractor(get(), get()) }
Expand All @@ -59,4 +63,5 @@ fun commonModule() = module {
factory { IOSServicesScreenViewInteractor(get()) }
factory { CapabilityScreenViewInteractor(get()) }
factory { ColorPickerViewInteractor() }
factory { KVStoreDemoScreenViewInteractor(get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ class AppCoordinator(): Coordinator(
fun colorPickerClicked() = push(Route.ColorPicker)
fun htmlDemoClicked() = push(Route.WebDemo)
fun windowInfoClicked() = push(Route.WindowInfo)
fun kvStorelicked() = push(Route.KVStore)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package service.kvstoreDemo

import com.outsidesource.oskitkmp.filesystem.KmpFsRef
import com.outsidesource.oskitkmp.filesystem.KmpFsType
import com.outsidesource.oskitkmp.outcome.Outcome
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import ui.kvstoreDemo.KVStoreDemoScreenViewState

interface IKVStoreDemoService {
suspend fun observeTodos(): Flow<List<TodoItem>?>
suspend fun addTodoItem(title: String): Outcome<TodoItem, Any>
suspend fun removeTodoItem(id: String): Boolean
suspend fun changeState(id: String, completed: Boolean): Boolean
suspend fun rename(id: String, name: String): Boolean
}

@Serializable
data class TodoItem(
val id: String,
val name: String,
val completed: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package service.kvstoreDemo

import com.outsidesource.oskitkmp.outcome.Outcome
import com.outsidesource.oskitkmp.outcome.unwrapOrNull
import com.outsidesource.oskitkmp.storage.IKmpKvStore
import com.outsidesource.oskitkmp.storage.IKmpKvStoreNode
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.serialization.builtins.ListSerializer

private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

private const val KEY_ITEMS = "items"


/**
* A simple KVStore demo service that demonstrates how to use KmpKvStore to store and retrieve data in a reactive
* way. Note, the implementation here is simplified for the demo purposes.
*
* The KVStoreDemoService is a simple service that stores and retrieves TodoItems in a reactive way. It uses a single
* KmpKvStore node to store the serialized list of [TodoItem]. The [KmpKvStore] node is opened asynchronously when the service is
* initialized. The [KmpKvStore] node is closed when the service is destroyed.
*
* The [KVStoreDemoService] provides a flow of a list of [TodoItem] that can be observed using the observeTodos() function.
*
* The [KVStoreDemoService] provides functions to add, remove, update, and rename TodoItems using the addTodoItem(),
* removeTodoItem(), changeState(), and rename() functions, respectively.
*
* This service is being consumed in the [ui.kvstoreDemo.KVStoreDemoScreenViewInteractor].
*/
class KVStoreDemoService(
private val storage: IKmpKvStore,
) : IKVStoreDemoService {

private val node = CompletableDeferred<IKmpKvStoreNode?>()

init {
scope.launch {
node.complete(storage.openNode("kvstoredemo").unwrapOrNull())
}
}

override suspend fun observeTodos(): Flow<List<TodoItem>?> {
return node.await()?.observeSerializable(KEY_ITEMS, ListSerializer(TodoItem.serializer())) ?: emptyFlow()
}

override suspend fun addTodoItem(title: String): Outcome<TodoItem, Any> {
val data = readItemsSnapshot()
val entity = TodoItem(title, title, false)
val res = writeItemsSnapshot(data.toMutableList().apply { add(entity) })

return when {
res != null && res is Outcome.Ok -> Outcome.Ok(entity)
res is Outcome.Error -> Outcome.Error(res.error)
else -> Outcome.Error(IllegalStateException("Node is null"))
}
}

override suspend fun removeTodoItem(id: String): Boolean {
val data = readItemsSnapshot().toMutableList()
val item = data.find { it.id == id }

return if (item != null) {
data.remove(item)
writeItemsSnapshot(data)
true
} else {
false
}
}

override suspend fun changeState(id: String, completed: Boolean): Boolean {
val data = readItemsSnapshot().toMutableList()
val item = data.find { it.id == id }

return if (item != null) {
val index = data.indexOf(item)
data[index] = item.copy(completed = completed)
writeItemsSnapshot(data)
true
} else {
false
}
}

override suspend fun rename(id: String, name: String): Boolean {
val data = readItemsSnapshot().toMutableList()
val item = data.find { it.id == id }

return if (item != null) {
val index = data.indexOf(item)
data[index] = item.copy(name = name)
writeItemsSnapshot(data)
true
} else {
false
}
}

private suspend fun readItemsSnapshot(): List<TodoItem> {
return node.await()?.getSerializable(
KEY_ITEMS,
ListSerializer(TodoItem.serializer())
).orEmpty().toMutableList()
}

private suspend fun writeItemsSnapshot(data: List<TodoItem>): Outcome<Unit, Any>? {
return node.await()?.putSerializable(KEY_ITEMS, data, ListSerializer(TodoItem.serializer()))
}

}
1 change: 1 addition & 0 deletions composeApp/src/commonMain/kotlin/ui/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sealed class Route(
data object ColorPicker : Route(webRoutePath = "/color-picker")
data object WebDemo : Route(webRoutePath = "/web-demo")
data object WindowInfo : Route(webRoutePath = "/window-info")
data object KVStore : Route(webRoutePath = "/kvstore")

companion object {
val deepLinks = Router.buildDeepLinks {
Expand Down
2 changes: 2 additions & 0 deletions composeApp/src/commonMain/kotlin/ui/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ui.markdown.MarkdownScreen
import ui.popups.PopupsScreen
import ui.viewStateExample.ViewStateExampleScreen
import ui.htmlDemo.HtmlDemoScreen
import ui.kvstoreDemo.KVStoreDemoScreen
import ui.widgets.WidgetsScreen
import ui.windowInfo.WindowInfoScreen

Expand Down Expand Up @@ -62,6 +63,7 @@ fun App(
RouteTransitionDirection.Out
}
)
is Route.KVStore -> KVStoreDemoScreen()
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ fun HomeScreen(
onClick = interactor::htmlDemoButtonClicked,
enabled = Platform.current == Platform.WebBrowser,
)
Button(
content = { Text("KV Store Demo") },
onClick = interactor::kvStoreButtonClicked,
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ class HomeViewInteractor(
fun colorPickerButtonClicked() = coordinator.colorPickerClicked()
fun htmlDemoButtonClicked() = coordinator.htmlDemoClicked()
fun windowInfoButtonClicked() = coordinator.windowInfoClicked()
fun kvStoreButtonClicked() = coordinator.kvStorelicked()
}
Loading