Marcello Galhardo
06/14/2023, 10:09 AMRow
, Column
, and similar view groups but it is heavily customisable. Basically, you can do something like:
// Definition, kind of:
@resultBuilder
struct SettingsBuilder {
static func buildBlock() -> [Setting] { [] }
}
func makeSettings(@SettingsBuilder _ content: () -> [Setting]) -> [Setting] {
content()
}
// Usage, each line is an item in the array:
makeSettings {
Setting(name: "Offline mode", value: .bool(false))
Setting(name: "Search page size", value: .int(25))
}
That would be similar to what we have with Compose groups, I guess:
Row {
Text("Offline mode")
Second("Search page size")
}
The nice part is that you can create and customize your own “view group”-like DSL to build your APIs. Is there anything similar in the plans for Kotlin?franztesca
06/14/2023, 10:24 AMdata class Setting(val name: String, val value: Any)
val settings = buildList {
add(Setting(name = "Offline mode", value = false))
add(Setting(name = "Search page size", value = 25))
}
or you can add your semantic wrapper if you really like and achieve
val settings = makeSettings {
setting(...)
setting(...)
}
easily, alreadymarstran
06/14/2023, 10:27 AMdata class Setting<T>(val name: String, val value: T)
class SettingsBuilder {
val list = mutableListOf<Setting<*>>()
fun <T> setting(name: String, value: T) =
list.add(Setting(name, value))
}
fun makeSettings(builder: SettingsBuilder.() -> Unit): List<Setting<*>> =
SettingsBuilder().apply { builder() }.list
fun main() {
val settings = makeSettings {
setting("Offline mode", false)
setting("Search page size", 25)
}
println(settings)
}
Marcello Galhardo
06/14/2023, 2:38 PMadd
(like in a normal builder
). If you nest two types inside each other (let’s say, a Row, inside a Column, inside a…), you would end up with a lot of indentation and a confusing syntax IMO.
Row {
add {
Column {
add {
// etc...
}
}
}
}
ResultBuilders
let you write your own “builders like” without this verbosity, which can be extended to any API (I think today Compose does that with a Compiler plugin… but that could be at language level and seems interesting to make more complex DSLs with inner collections).
Each expression (line), is added to the collection that later can be traversed.Marcello Galhardo
06/14/2023, 2:40 PMadd { }
, seems better.Marcello Galhardo
06/14/2023, 2:41 PMYoussef Shoaib [MOD]
06/14/2023, 3:49 PM// This would exist in a library somewhere, just like how it's in Rust's stdlib
// Made it inline because why not!
@JvmInline value class ResultBuilder<T>(private val list: MutableList<T>) {
operator fun T.unaryPlus() { list.add(this) }
}
// Convenience function
inline fun <T> makeResult(content: ResultBuilder<T>.() -> Unit) = buildList {
ResultBuilder(this).content()
}
// In some UI library
typealias SettingsBuilder = ResultBuilder<Setting<*>>
fun makeSettings(content: SettingsBuilder.() -> Unit): List<Setting<*>> = makeResult(content)
// Only do this if Setting should only be created inside a `makeSettings` block,
// otherwise use the + operator
data class Setting<out T> private constructor(val name: String, val value: T) {
companion object {
context(SettingsBuilder)
operator fun <T> invoke(name: String, value: T) { +Setting(name, value) }
}
}
fun main() {
// Usage, each line adds an item to the list
println(makeSettings {
Setting(name = "Offline mode", value = false)
Setting(name = "Search page size", value = 25)
})
//Setting(name = "Offline mode", value = false) // Not allowed
}
Youssef Shoaib [MOD]
06/14/2023, 4:05 PM// This would exist in a library somewhere, just like how it's in Swift's stdlib
@JvmInline value class Builder<in T>(private val list: MutableList<T>) {
operator fun T.unaryPlus() { list.add(this) }
}
// Convenience function
inline fun <T> build(@BuilderInference block: Builder<T>.() -> Unit) = buildList {
Builder(this).block()
}
// In some UI library
data class Setting<out T> constructor(val name: String, val value: T)
context(Builder<Setting<T>>)
fun <T> setting(name: String, value: T) {
+Setting(name, value)
}
// Perhaps this is a weird default where you want +"SettingName" to add a setting with a boolean value by default
context(Builder<Setting<Boolean>>)
operator fun String.unaryPlus() {
+Setting(this, false)
}
fun main() {
// Usage, each line adds an item to the list
// Type inference does the heavy lifting here
println(build {
setting(name = "Offline mode", value = false)
+Setting(name = "Search page size", value = 25)
})
// or you can specify the type yourself, especially when you have such overloads as above
println(build<Setting<*>> {
+Setting(name = "Offline mode", value = false)
+Setting(name = "Search page size", value = 25)
+"Online visibility on"
})
}
Youssef Shoaib [MOD]
06/14/2023, 4:19 PM+
and setting
be resolved automatically to the right thing and require that it's inside a Builder
. The way Kotlin does it means that it plays nicely with all the other language features, while it looks like that Swift had to disallow a whole slew of built-in functions inside of result builders.Marcello Galhardo
06/14/2023, 4:25 PMunaryPlus
til (I would still prefer without but that seems a valid approach without complicate anything). To be sure everyone is the same page, so the unaryPlus
would be the “Kotlin idiomatic way” to do generic “result builders”. Is that correct?Youssef Shoaib [MOD]
06/14/2023, 4:40 PMunaryPlus
as long as you define methods like setting()
or make the constructors for your data types internal and have factory functions (like a fun Setting()
), or you can make the constructor private and have a operator invoke
on the companion object as per my first example. All 3 of those methods would take a context(Builder<Blah>)
, and so they'll be able to add the values to the array, but they can do so, so much more without ruining the semantics or composability of the language. In fact, compose existed before we had those context receivers, and so it had to get around some of those issues with a compiler plugin (and compose also does some magic to do efficient recomposition, but the basic UI-tree building aspect of it is absolutely possible in pure Kotlin. See also kotlinx.html for an HTML DSL library that might provide some inspiration