Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 kvStoreClicked() = 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 @@ -102,6 +102,10 @@ fun HomeScreen(
content = { Text("Window Info") },
onClick = interactor::windowInfoButtonClicked,
)
Button(
content = { Text("KV Store Demo") },
onClick = interactor::kvStoreButtonClicked,
)
Button(
content = { Text(rememberKmpString(Strings.iosServices)) },
onClick = interactor::iosServicesButtonClicked,
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.kvStoreClicked()
}
Loading