Lychee
The most complete and powerful data-binding library and persistence infra for Kotlin 1.5, Android & Splitties Views DSL, JavaFX & TornadoFX, JSON, JDBC & SQLite, HTTP, SharedPreferences.
Install / Use
/learn @Miha-x64/LycheeREADME
Lychee (ex. reactive-properties)
Lychee is a library to rule all the data.
ToC
- Approach to declaring data
- Properties
- Other data-binding libraries
- Properties sample
- Sample usage in GUI application
- Persistence and Android
- SQL <!-- TODO other SQL libraries -->
- HTTP
- FAQ
- Adding to a project
Approach to declaring data
Typically, we declare data using classes:
/*data*/ class Player(
val name: String,
val surname: String,
var score: Int,
)
But there are some mistakes in the example above:
-
there aren't any convenient ways to manipulate the properties of arbitrary classes:
- reflection does not play well with ProGuard/R8 and Graal,
- kapt is slow and does not play well with separate compilation,
- both break encapsulation by exposing field names (or values of annotations) as serialization interface,
- none of them knows precisely how to serialize values, they just try to guess according to field types,
- there are no standard annotations:
every JSON library has its own annotations (Gson:
@SerializedNameand@TypeAdapter), every ORM, ActiveRecord, or another database-related thing has its own, too, TypeAdapterconcept is cursed: every library tries to support all standard types (how often do you need to storeAtomicIntegerArray? Gson has built-in support for it, this is crazy!), and tree shakers (a.k.a. dead code eliminators) cannot figure out which ones are actually used (this requires deep understanding of reflection API andMap<Type, TypeAdapter>machinery, 10+ years of ProGuard were not enough to get this deep);
-
along with interface (property names and types), this also declares implementation details (backing fields). Thus, you're getting only in-memory representation. (To workaround the issue, Realm, for example, extends your classes so getters&setters are overridden while fields are unused, and rewrites your bare field accesses, if any, to use getters&setters.) Theoretically, this can be fixed by extracting
interface:interface Player { val name: String val surname: String var score: Int // fun copy()? can we also ask to implement equals()? no. } data class MemoryPlayer(override val …) : Player class JsonPlayer(private val json: JsonObject) : Player { override val name: String get() = json.getString("name") … } class SqlPlayer(private val connection: Connection) : Player { override val name: String get() = connection.createStatement()… }but implementations are 146% boilerplate;
-
no mutability control.
var scoreis mutable but not observable; -
hashCode,equals, andtoStringcontain generated bytecode fully consisting of boilerplate; -
data class
copynot only consists of boilerplate but also becomes binary incompatible after every primary constructor change; -
data class
componentNs are pure evil in 99% cases: destructuring is good with positional things likePairorTriplebut not with named properties.
:persistence module provides the solution. Interface is declared by inheriting Schema:
object Player : Schema<Player>() {
val Name = "name" let string
val Surname = "surname" let string
val Score = "score".mut(i32, default = 0)
}
Here, Player, string, and i32 (not int because it's Java keyword) are all subtypes of DataType.
Thus, they declare how to store data both in-memory and on wire.
Name, Surname, and Score are field definitions, two immutable and one mutable,
based on typed key pattern.
Implementations are subtypes of Struct<SCHEMA>,
so they implement storage machinery while staying decoupled
from data schema:
val player: StructSnapshot<Player> = Player { p ->
p[Name] = "John"
p[Surname] = "Galt"
// Score gets its default value.
}
StructSnapshot is immutable (and very cheap: it is an Array, not a HashMap) implementation. It can only be read from:
assertEquals(0, player[Player.Score])
Here, Player {} is SCHEMA.invoke(build: SCHEMA.() -> Unit) function which tries to mimic struct literal;
p is StructBuilder<SCHEMA>–a fully mutable temporary object.
Structs implement hashCode, equals, toString, and copy of this kind: player.copy { it[Score] = 9000 }.
It creates new StructBuilder and passes it to the function you provide.
(Similar thing is called newBuilder in OkHttp, and buildUpon in android.net.Uri.)
There's also a good practice to implement a constructor function which gives less chance of forgetting to specify required field values:
fun Player(name: String, surname: String) = Player { p ->
p[Name] = name
p[Surname] = surname
}
Properties
Properties (subjects, observables) inspired by JavaFX
and Vue.js MVVM-like approach are available in :properties module.
A Property provides functionality similar to
BehaviorSubject in RxJava, or Property in JavaFX,
or LiveData in Android Arch.
- Simple and easy-to-use
- Lightweight: persistence + properties + android-bindings define around 1500 methods
including easy-to-shrink
inline funs andvalue classes - zero reflection <small>(the only use of kotlin.reflect is required if you delegate your Kotlin property to a Lychee
Propertyand eliminated by Kotlin 1.3.70+ compiler)</small> - Extensible: not confined to Android, JavaFX or whatever (want MPP? File an issue with sample use-cases)
- Single-threaded and concurrent (lock-free) implementations
- Ready to use Android bindings like
tv.bindTextTo(prop), notld.observe(viewLifecycleOwner) { tv.text = it } - Some bindings for JavaFX
- Sweet with View DSLs like Splitties and TornadoFX
- Depends only on Kotlin-stdlib and
Kotlin-MPP Collection utils for overheadless
EnumSets - Presentation about properties: initial problem statement and some explanations
With :persistence + :properties, it's also possible to observe mutable fields:
val observablePlayer = ObservableStruct(player)
val scoreProp: Property<Int> = observablePlayer prop Player.Score
someTextView.bindTextTo(scoreProp.map(CharSequencez.ValueOf)) // bind to UI, for example
// both mutate the same text in-memory int value and a text field:
scoreProp.value = 10
observablePlayer[Player.Score] = 20
Other data-binding libraries
to explain why I've rolled my own:
-
agrosner/KBinding (MIT): similar to this, Observable-based, Android-only, depends on kotlinx.coroutines
-
BennyWang/KBinding (no license): Android-only, uses annotation processing, depends on RXJava 1.3
-
LewisRhine/AnkoDataBindingTest (no license): proof of concept solution from Data binding in Anko article, Android-only, depends on Anko and AppCompat
-
lightningkite/kotlin-anko-observable (no license): Android-only, supports easy creation of RecyclerView adapters along with data-binding, based on lightningkite/kotlin-anko (depends on Anko and AppCompat) and lightningkite/kotlin-observable (
ObservableProperty<T>andObservableList<T>); UnknownJoe796/kotlin-components-starter (MIT) -
MarcinMoskala/KotlinAndroidViewBindings (Apache 2.0): delegates properties of Views-by-id to to Kotlin properties
Properties sample
val prop: MutableProperty<Int> = propertyOf(1)
val mapped: Property<Int> = prop.map { 10 * it }
assertEquals(10, mapp
Related Skills
feishu-drive
345.9k|
things-mac
345.9kManage Things 3 via the `things` CLI on macOS (add/update projects+todos via URL scheme; read/search/list from the local Things database)
clawhub
345.9kUse the ClawHub CLI to search, install, update, and publish agent skills from clawhub.com
postkit
PostgreSQL-native identity, configuration, metering, and job queues. SQL functions that work with any language or driver
