Replace Custom Observer with kotlinx StateFlows

Signed-off-by: Chaoscaot <max@maxsp.de>
This commit is contained in:
2026-07-06 01:13:05 +02:00
parent 62e855ec1b
commit 3a247de43d
9 changed files with 162 additions and 114 deletions
+2 -1
View File
@@ -36,6 +36,7 @@ dependencies {
compileOnly(libs.paperapi) compileOnly(libs.paperapi)
compileOnly(project(":SpigotCore")) compileOnly(project(":SpigotCore"))
implementation(libs.coroutinesCore)
implementation(libs.exposedCore) implementation(libs.exposedCore)
implementation(libs.exposedDao) implementation(libs.exposedDao)
implementation(libs.exposedJdbc) implementation(libs.exposedJdbc)
@@ -47,4 +48,4 @@ val compileKotlin: KotlinCompile by tasks
compileKotlin.compilerOptions { compileKotlin.compilerOptions {
freeCompilerArgs.set(listOf("-XXLanguage:+ContextParameters")) freeCompilerArgs.set(listOf("-XXLanguage:+ContextParameters"))
} }
@@ -1,101 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.kotlin.ui
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
abstract class Observable<T> {
internal val listeners = mutableSetOf<ObserverListener>()
open fun removeListener(render: ObserverListener) {
listeners.remove(render)
}
internal fun notifyListeners() {
listeners.forEach { it.update() }
}
context(render: ObserverListener)
fun listen(): Observable<T> {
listeners.add(render)
render.observers.add(this)
return this
}
fun listen(callback: (T) -> Unit): () -> Unit {
val listener = object: ObserverListener() {
override fun update() = callback(get())
}
listeners.add(listener)
callback(get())
return { removeListener(listener) }
}
abstract fun get(): T
fun <R> map(mapper: (T) -> R): Observable<R> = Delegate(mapper, this)
class Delegate<T, R>(val mapper: (T) -> R, val parent: Observable<T>): Observable<R>(), ReadOnlyProperty<Any?, R> {
var value: R? = null
var unsubParent = parent.listen {
val old = value
value = mapper(it)
if (old != value) {
notifyListeners()
}
}
override fun removeListener(render: ObserverListener) {
super.removeListener(render)
if (listeners.isEmpty()) {
unsubParent()
}
}
override fun get(): R = value!!
override fun getValue(thisRef: Any?, property: KProperty<*>): R = value!!
}
}
class Observer<T>(private var value: T): ReadWriteProperty<Any?, T>, Observable<T>() {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
notifyListeners()
}
fun set(value: T) {
this.value = value
notifyListeners()
}
override fun get() = value
fun update(update: (T) -> T) {
this.value = update(value)
notifyListeners()
}
}
@@ -0,0 +1,90 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.kotlin.ui
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.reflect.KProperty
private val uiStateScope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
context(render: StateFlowListener)
fun <T> StateFlow<T>.listen(): StateFlow<T> {
render.track(this) {
drop(1).onEach { render.update() }.launchIn(uiStateScope)
}
return this
}
context(render: StateFlowListener)
fun <T> MutableStateFlow<T>.listen(): MutableStateFlow<T> {
render.track(this) {
drop(1).onEach { render.update() }.launchIn(uiStateScope)
}
return this
}
fun <T> StateFlow<T>.listen(callback: (T) -> Unit): () -> Unit {
callback(value)
val job = drop(1).onEach { callback(it) }.launchIn(uiStateScope)
return { job.cancel() }
}
operator fun <T> StateFlow<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
operator fun <T> MutableStateFlow<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
fun <T, R> StateFlow<T>.map(mapper: (T) -> R): StateFlow<R> = MappedStateFlow(this, mapper)
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
private class MappedStateFlow<T, R>(
private val parent: StateFlow<T>,
private val mapper: (T) -> R,
): StateFlow<R> {
override val replayCache: List<R>
get() = listOf(value)
override val value: R
get() = mapper(parent.value)
override suspend fun collect(collector: FlowCollector<R>): Nothing {
var initialized = false
var previous: Any? = null
parent.collect {
val mapped = mapper(it)
if (!initialized || previous != mapped) {
initialized = true
previous = mapped
collector.emit(mapped)
}
}
}
}
@@ -19,13 +19,23 @@
package de.steamwar.kotlin.ui package de.steamwar.kotlin.ui
abstract class ObserverListener { import kotlinx.coroutines.Job
val observers = mutableSetOf<Observable<*>>()
abstract class StateFlowListener {
private val jobs = mutableMapOf<Any, Job>()
internal fun track(key: Any, createJob: () -> Job) {
if (key in jobs) return
val job = createJob()
jobs[key] = job
job.invokeOnCompletion { jobs.remove(key) }
}
abstract fun update() abstract fun update()
open fun destroy() { open fun destroy() {
observers.forEach { it.removeListener(this) } jobs.values.toList().forEach { it.cancel() }
observers.clear() jobs.clear()
} }
} }
@@ -0,0 +1,46 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.kotlin.ui
import de.steamwar.kotlin.ui.components.item
import de.steamwar.kotlin.util.count
import kotlinx.coroutines.flow.MutableStateFlow
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
val Counter = MutableStateFlow(0)
class TestInv(player: Player): UIInventory(player) {
override fun view() {
inventory(3, "") {
item {
val amount by Counter.map { it + 1 }.listen()
item = ItemStack.of(Material.STONE)
.count(amount)
x = 0
y = 0
onClick {
Counter.value += 1
}
}
}
}
}
@@ -23,7 +23,7 @@ import de.steamwar.kotlin.ui.context.WindowContext
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryType import org.bukkit.event.inventory.InventoryType
abstract class UIInventory(val player: Player): ObserverListener() { abstract class UIInventory(val player: Player): StateFlowListener() {
var window: UIWindow? = null var window: UIWindow? = null
abstract fun view() abstract fun view()
@@ -52,4 +52,4 @@ abstract class UIInventory(val player: Player): ObserverListener() {
protected fun inventory(type: InventoryType, title: String, init: WindowContext.() -> Unit) { protected fun inventory(type: InventoryType, title: String, init: WindowContext.() -> Unit) {
window = UIWindow(type, title, player, init) window = UIWindow(type, title, player, init)
} }
} }
@@ -19,7 +19,7 @@
package de.steamwar.kotlin.ui.components package de.steamwar.kotlin.ui.components
import de.steamwar.kotlin.ui.ObserverListener import de.steamwar.kotlin.ui.StateFlowListener
import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.RenderMarker
import de.steamwar.kotlin.ui.RenderObject import de.steamwar.kotlin.ui.RenderObject
import de.steamwar.kotlin.ui.context.GroupContext import de.steamwar.kotlin.ui.context.GroupContext
@@ -29,7 +29,7 @@ import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@RenderMarker @RenderMarker
class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderObject, ObserverListener() { class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderObject, StateFlowListener() {
override fun update() { override fun update() {
val oldX = x val oldX = x
val oldY = y val oldY = y
@@ -40,6 +40,7 @@ class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Un
} }
override fun destroy() { override fun destroy() {
super.destroy()
parent.resetSlot(x, y) parent.resetSlot(x, y)
} }
@@ -19,14 +19,14 @@
package de.steamwar.kotlin.ui.context package de.steamwar.kotlin.ui.context
import de.steamwar.kotlin.ui.ObserverListener import de.steamwar.kotlin.ui.StateFlowListener
import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.RenderMarker
import de.steamwar.kotlin.ui.RenderObject import de.steamwar.kotlin.ui.RenderObject
import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@RenderMarker @RenderMarker
open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): ObserverListener(), RenderObject, RenderParent { open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): StateFlowListener(), RenderObject, RenderParent {
val children = mutableListOf<RenderObject>() val children = mutableListOf<RenderObject>()
val updatedSlots = mutableSetOf<Pair<Int, Int>>() val updatedSlots = mutableSetOf<Pair<Int, Int>>()
@@ -62,4 +62,4 @@ open class GroupContext(val parent: RenderParent?, val init: GroupContext.() ->
init(this) init(this)
(oldUpdatedSlots - updatedSlots).forEach { resetSlot(it.first, it.second) } (oldUpdatedSlots - updatedSlots).forEach { resetSlot(it.first, it.second) }
} }
} }
+1
View File
@@ -128,6 +128,7 @@ dependencyResolutionManagement {
library("msgpack", "org.msgpack:msgpack-core:0.9.8") library("msgpack", "org.msgpack:msgpack-core:0.9.8")
library("logback", "ch.qos.logback:logback-classic:1.5.6") library("logback", "ch.qos.logback:logback-classic:1.5.6")
library("coroutinesCore", "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
val ktorVersion = "2.3.12" val ktorVersion = "2.3.12"