|
1 |
| -## Data-compat |
| 1 | +## Kotlin-data-compat |
2 | 2 |
|
3 | 3 | ### Context
|
4 |
| -The Data-compat library resolves the Java binary incompatibility that library developers face when using Kotlin data classes. In a nutshell, every change to a data class results in major breaking change. This project attempts to resolve this binary incompatibility by using annotation processing. Users can keep using their original data classes, but the generated code will compatible for Java consumption. |
5 |
| - |
| 4 | +The Data-compat library resolves the Java binary incompatibility that library developers face when using Kotlin data classes. In a nutshell, every change to a data class results in major breaking change. This project attempts to resolve this binary incompatibility by using annotation processing. Users can keep using their original data classes definitions, but the generated code will compatible for Java consumption. |
6 | 5 | To read more about this incompatibility, please refer to Jake Wharton's [Public API challenges in Kotlin](https://jakewharton.com/public-api-challenges-in-kotlin/) blogpost.
|
7 | 6 |
|
8 |
| -### How |
9 |
| -Data-compat uses [Kotlin Symbol Processing API (KSP)](https://kotlinlang.org/docs/ksp-overview.html) in combination with [KotlinPoet](https://square.github.io/kotlinpoet/) to generate Kotlin classes that are Java binary compatible. Input for the system is a private data class that supports a `@DataCompat` annotation, library developers can easily convert their existing data classes but make them private and add the required annotation. The code generator will output a Kotlin class that supports a builder pattern compatible for Java usage. |
| 7 | +This library uses [Kotlin Symbol Processing API (KSP)](https://kotlinlang.org/docs/ksp-overview.html) in combination with [KotlinPoet](https://square.github.io/kotlinpoet/) to generate Kotlin classes. Input is a private data class that supports a `@DataCompat` annotation and the code generator outputs a Kotlin class that supports a builder pattern compatible for Java usage. |
| 8 | + |
| 9 | +### Add to your project |
| 10 | +The project is hosted on [jitpack](https://jitpack.io/) and requires to add jitpack.io lookup to your gradle configuration: |
| 11 | + |
| 12 | +```groovy |
| 13 | +allprojects { |
| 14 | + repositories { |
| 15 | + maven { url 'https://jitpack.io' } |
| 16 | + } |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +Since this project uses ksp, you will have to include it: |
| 21 | +```groovy |
| 22 | +plugins { |
| 23 | + id 'com.google.devtools.ksp' version '1.6.10-1.0.2' apply false |
| 24 | +} |
| 25 | +``` |
| 26 | + |
| 27 | +And you will have to include the required dependencies: |
| 28 | + |
| 29 | +```groovy |
| 30 | +dependencies { |
| 31 | + implementation 'com.github.tobrun.kotlin-data-compat:annotation:0.1.0' |
| 32 | + ksp 'com.github.tobrun.kotlin-data-compat:processor:0.1.0' |
| 33 | +} |
| 34 | +``` |
| 35 | + |
10 | 36 |
|
11 |
| -#### Input |
| 37 | +### Getting started |
| 38 | + |
| 39 | +Given an exisiting data class: |
| 40 | + - add `@DataCompat` annotation |
| 41 | + - mark class private |
| 42 | + - Append `Data` to the class name |
| 43 | + |
| 44 | +For example: |
12 | 45 |
|
13 | 46 | ```kotlin
|
| 47 | +/** |
| 48 | + * Represents a person. |
| 49 | + * @property name The full name. |
| 50 | + * @property nickname The nickname. |
| 51 | + * @property age The age. |
| 52 | + */ |
14 | 53 | @DataCompat
|
15 | 54 | private data class PersonData(val name: String, val nickname: String? = null, val age: Int)
|
16 | 55 | ```
|
17 | 56 |
|
18 |
| -#### Output |
| 57 | +After compilation, the following class will be generated: |
19 | 58 |
|
20 | 59 | ```kotlin
|
21 |
| -class Person private constructor( |
22 |
| - val name: String, |
23 |
| - val nickname: String?, |
24 |
| - val age: Int |
| 60 | +/** |
| 61 | + * Represents a person. |
| 62 | + * @property name The full name. |
| 63 | + * @property nickname The nickname. |
| 64 | + * @property age The age. |
| 65 | + */ |
| 66 | +public class Person private constructor( |
| 67 | + public val name: String, |
| 68 | + public val nickname: String?, |
| 69 | + public val age: Int |
25 | 70 | ) {
|
26 |
| - override fun toString() = "Person(name=$name, nickname=$nickname, age=$age)" |
27 |
| - override fun equals(other: Any?) = other is Person |
28 |
| - && name == other.name |
29 |
| - && nickname == other.nickname |
30 |
| - && age == other.age |
31 |
| - override fun hashCode() = Objects.hash(name, nickname, age) |
32 |
| - |
33 |
| - class Builder { |
34 |
| - @set:JvmSynthetic // Hide 'void' setter from Java |
35 |
| - var name: String? = null |
36 |
| - @set:JvmSynthetic // Hide 'void' setter from Java |
37 |
| - var nickname: String? = null |
38 |
| - @set:JvmSynthetic // Hide 'void' setter from Java |
39 |
| - var age: Int = 0 |
40 |
| - |
41 |
| - fun setName(name: String?) = apply { this.name = name } |
42 |
| - fun setNickname(nickname: String?) = apply { this.nickname = nickname } |
43 |
| - fun setAge(age: Int) = apply { this.age = age } |
44 |
| - |
45 |
| - fun build() = Person(name!!, nickname, age) |
| 71 | + public override fun toString() = "Person(name=$name, nickname=$nickname, age=$age)" |
| 72 | + |
| 73 | + public override fun equals(other: Any?): Boolean = other is Person |
| 74 | + && name == other.name |
| 75 | + && nickname == other.nickname |
| 76 | + && age == other.age |
| 77 | + |
| 78 | + public override fun hashCode(): Int = Objects.hash(name, nickname, age) |
| 79 | + |
| 80 | + /** |
| 81 | + * Composes and builds a [Person] object. |
| 82 | + * |
| 83 | + * This is a concrete implementation of the builder design pattern. |
| 84 | + * |
| 85 | + * @property name The full name. |
| 86 | + * @property nickname The nickname. |
| 87 | + * @property age The age. |
| 88 | + */ |
| 89 | + public class Builder { |
| 90 | + @set:JvmSynthetic |
| 91 | + public var name: String? = null |
| 92 | + |
| 93 | + @set:JvmSynthetic |
| 94 | + public var nickname: String? = null |
| 95 | + |
| 96 | + @set:JvmSynthetic |
| 97 | + public var age: Int? = null |
| 98 | + |
| 99 | + /** |
| 100 | + * Set the full name. |
| 101 | + * |
| 102 | + * @param name the full name. |
| 103 | + * @return Builder |
| 104 | + */ |
| 105 | + public fun setName(name: String?): Builder { |
| 106 | + this.name = name |
| 107 | + return this |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Set the nickname. |
| 112 | + * |
| 113 | + * @param nickname the nickname. |
| 114 | + * @return Builder |
| 115 | + */ |
| 116 | + public fun setNickname(nickname: String?): Builder { |
| 117 | + this.nickname = nickname |
| 118 | + return this |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * Set the age. |
| 123 | + * |
| 124 | + * @param age the age. |
| 125 | + * @return Builder |
| 126 | + */ |
| 127 | + public fun setAge(age: Int?): Builder { |
| 128 | + this.age = age |
| 129 | + return this |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * Returns a [Person] reference to the object being constructed by the builder. |
| 134 | + * |
| 135 | + * Throws an [IllegalArgumentException] when a non-null property wasn't initialised. |
| 136 | + * |
| 137 | + * @return Person |
| 138 | + */ |
| 139 | + public fun build(): Person { |
| 140 | + if (name==null) { |
| 141 | + throw IllegalArgumentException("Null name found when building Person.") |
| 142 | + } |
| 143 | + if (age==null) { |
| 144 | + throw IllegalArgumentException("Null age found when building Person.") |
| 145 | + } |
| 146 | + return Person(name!!, nickname, age!!) |
| 147 | + } |
46 | 148 | }
|
47 | 149 | }
|
48 | 150 |
|
49 |
| -@JvmSynthetic // Hide from Java callers who should use Builder. |
50 |
| -fun Person(initializer: Person.Builder.() -> Unit): Person { |
51 |
| - return Person.Builder().apply(initializer).build() |
52 |
| -} |
| 151 | +/** |
| 152 | + * Creates a [Person] through a DSL-style builder. |
| 153 | + * |
| 154 | + * @param initializer the intialisation block |
| 155 | + * @return Person |
| 156 | + */ |
| 157 | +@JvmSynthetic |
| 158 | +public fun Person(initializer: Person.Builder.() -> Unit): Person = |
| 159 | + Person.Builder().apply(initializer).build() |
53 | 160 | ```
|
54 |
| - |
55 |
| -### State of project |
56 |
| - |
57 |
| -This project is under development, the following still needs to be done: |
58 |
| - - [ ] integrate output compatible with above using Kotlin Poet |
59 |
| - - [ ] split up annotation from processor module? |
60 |
| - - [ ] integrate example shown above |
61 |
| - - [ ] add unit tests |
62 |
| - - [ ] publish to maven |
|
0 commit comments