SkillAgentSearch skills...

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/Lychee
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Build Status Extremely lightweight Hits-of-Code Kotlin 1.5 Awesome Kotlin Channel at Kotlin Slack Telegram chat

<!-- abandoned [![Codacy Badge](https://api.codacy.com/project/badge/Grade/89813e3ee28441b3937a76f09e906aef)](https://www.codacy.com/app/Miha-x64/Lychee?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Miha-x64/Lychee&amp;utm_campaign=Badge_Grade) --> <!-- abandoned [![codecov](https://codecov.io/gh/Miha-x64/Lychee/branch/master/graph/badge.svg)](https://codecov.io/gh/Miha-x64/Lychee) in module `:properties`, excluding inline functions -->

Lychee (ex. reactive-properties)

Lychee is a library to rule all the data.

ToC

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: @SerializedName and @TypeAdapter), every ORM, ActiveRecord, or another database-related thing has its own, too,
    • TypeAdapter concept is cursed: every library tries to support all standard types (how often do you need to store AtomicIntegerArray? 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 and Map<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 score is mutable but not observable;

  • hashCode, equals, and toString contain generated bytecode fully consisting of boilerplate;

  • data class copy not 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 like Pair or Triple but 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 and value classes
  • zero reflection <small>(the only use of kotlin.reflect is required if you delegate your Kotlin property to a Lychee Property and 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), not ld.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:

Properties sample

val prop: MutableProperty<Int> = propertyOf(1)
val mapped: Property<Int> = prop.map { 10 * it }
assertEquals(10, mapp

Related Skills

View on GitHub
GitHub Stars122
CategoryData
Updated3mo ago
Forks10

Languages

Kotlin

Security Score

97/100

Audited on Dec 23, 2025

No findings