Drill is an utility library that makes working with immutable data classes easier by generating their mutable counterpart that is later frozen back into their immutable form.
Create a data class
and annotate it with @Drill
.
@Drill
data class Person(val name: String)
When building, Drill will generate a mutable version of Person called PersonMutable
and an
extension function Person.mutate()
that works similar to default apply
from standard library.
You can now "mutate" your data class similar to kotlin copy(...)
method from within the mutate
block as if your class was mutable.
val person: Person = Person(name = "Hello")
val mutated: Person = person.mutate { name = "world" }
However, Drill advantages come at play when accessing deeply nested information.
Let's modify name to be a complex structure
@Drill
data class Name(val name: String, val surname: String)
@Drill
data class Person(val name: Name)
If we were to modify the surname using copy methods we would write:
val person: Person = Person(name = Name(name="Hello", surname="World")
val mutated: Person = person.copy(
name=person.name.copy(
surname="Ugly Copy"
)
)
Which becomes harder to read and to maintain as models get more complex and nesting level increases.
This is the main use case for drill, using the mutate
extension we can now write:
val person: Person = Person(name = Name(name="Hello", surname="World")
val mutated: Person = person.mutate {
name.surname = "Drill"
}
As well as nesting data classes lists and maps are also pretty common when describing models.
@Drill
data class ListItem(val text: String = "item")
@Drill
data class ListClass(
val list: List<ListItem>
)
In order to support mutable like syntax for this types Drill provides two new Types DrillList
and DrillMap
that implement like kotlin MutableList
and a MutableMap
respectively but perform some bookkeeping in order to maintain data classes copy method semantics.
This way, we can easily modify items inside lists as if they were mutable lists of mutable items.
val source = ListClass(listOf(ListItem()))
println(source) //ListClass(list=[ListItem(text=item)])
val mutated = source.mutate {
//Modify item 0 and add some new ones
list[0].text = "Hello I am first index"
list.add(ListItem("Second item"))
list.add(ListItem("Third item"))
}.mutate {
//Remove second item in second mutation
list.removeAt(1)
}
println(mutated) //ListClass(list=[ListItem(text=Hello I am first index), ListItem(text=Third item)])
Maps behave in a similar way:
val source = MapClass()
val newItem = MapItem("added")
val mutated = source.mutate {
this.map["a"] = newItem
}
Check more example usages in the test module
Only significant performance impact is one additional object allocation everytime a mutable object is read for the first time in a lazy fashion. This includes nested fields and items in both lists and maps.
anyObject.mutate { // implicit `this` mandatory allocation
field = "reference" // no object allocation
nested.another = "nested" // mutable object `nested` allocated
list.size // mutable list allocated
list[0].text = "list access" // mutable item [0] allocated
}
For that reason, you should avoid traversing mutables object inside the mutate
block if running in a critical section like a draw loop to prevent triggering a GC later down the line. Reverting back to regular copy
will always be possible since original classes are not modified in any way.
Semantics expected from mutable classes mimic behaviour from copy, including reference equality. That is, a non changed field will keep it's reference, so ===
operator will hold true for it's fields unless it was mutated.
For lists and maps, changing any item in the underlying collection will trigger list or map recreation
Add common library and annotation processor (with kapt
plugin) to your dependencies.
You can grab the latest version from github maven repository or jitpack:
Github Package Registry:
implementation("com.minikorp:drill:drill-common:$DRILL_VERSION")
kapt("com.minikorp:drill-processor:$DRILL_VERSION")
Jitpack:
repositories {
maven { url = uri("https://jitpack.io") }
}
implementation("com.github.minikorp.drill:drill-common:$DRILL_VERSION")
kapt("com.github.minikorp.drill:drill-processor:$DRILL_VERSION")