Part 1: A Metaprogramming Feast
Part 2: A Farewell To Stubs
Part 4: The Great KatSPy
Part 3: The Old Man and the IDE
GitHub project link
Part 1: A Metaprogramming Feast
Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data
@Inject
DI
Factory
etc.
@Before
fun setUp() {
// based on https://github.com/android/compose-samples/tree/main/Jetcaster
viewModel = HomeViewModel(
podcastsRepository = PodcastsRepository(
podcastsFetcher = PodcastsFetcher(
okHttpClient = mockClient,
syndFeedInput = SyndFeedInput(true, Locale.ROOT),
testCoroutineDispatcher,
),
podcastStore = PodcastStore(
podcastDao = fakeDatabase.podcastDao,
podcastFollowedEntryDao = fakeDatabase.podcastFollowedEntryDao,
transactionRunner = fakeDatabase.transactionRunner,
),
episodeStore = EpisodeStore(episodesDao = fakeDatabase.episodesDao),
categoryStore = CategoryStore(
categoriesDao = fakeDatabase.categoriesDao,
categoryEntryDao = fakeDatabase.podcastCategoryEntryDao,
episodesDao = fakeDatabase.episodesDao,
podcastsDao = fakeDatabase.podcastsDao,
),
transactionRunner = fakeDatabase.transactionRunner,
mainDispatcher = testCoroutineDispatcher
)
)
}
Feel the heavy lifting every time you manually instantiate a class in a test 🏋️♂️
🥵
class PodcastsRepository @Inject constructor(
private val podcastsFetcher: PodcastsFetcher,
private val podcastStore: PodcastStore,
private val episodeStore: EpisodeStore,
private val categoryStore: CategoryStore,
private val transactionRunner: TransactionRunner,
mainDispatcher: CoroutineDispatcher
)
Handle dependency injection as a cross-cutting concern using annotations
@HiltViewModel
class HomeViewModel @Inject constructor(
private val podcastsRepository: PodcastsRepository,
private val podcastStore: PodcastStore,
) : ViewModel()
Other metaprogramming heavy lifters
Epoxy
Room
🏋️♂️
Glide
Two roads diverged in a meta wood
reflection
code gen
Reflection, if you can @Keep it
😞 Reflection and minification are enemies
⚠️ Need to fail fast
🏋️♂️ kotlin.reflect is a large binary (2.9MB)
For large Android apps, "failing fast" means "failing at build time."
Runtime is too late.
Why not both? 🤔
⚡ Build fast like reflection
⚡ Fail fast code gen
Part 2: A Farewell To Stubs
Replaces legacy processors
🤔
It tolls for thee, data binding
🤔
A farewell to stubs? 🤔
Old sample using kapt version of Room
@androidx.room.Entity(tableName = "garden_plantings",
indices = {@androidx.room.Index(value = {"plant_id"})})
public final class GardenPlanting {
public final long getGardenPlantingId() {
return 0L;
}
public final void setGardenPlantingId(long p0) {
}
@org.jetbrains.annotations.NotNull()
@androidx.room.ColumnInfo(name = "plant_id")
private final java.lang.String plantId = null;
}
@Entity(
tableName = "garden_plantings",
indices = [Index("plant_id")]
)
data class GardenPlanting(
@ColumnInfo(name = "plant_id") val plantId: String,
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var gardenPlantingId: Long = 0
}
stubbed out
/* There are 4 steps to the compilation process: 1. Generate stubs (using kotlinc with kapt plugin which does no further compilation) 2. Run apt (using kotlinc with kapt plugin which does no further compilation) 3. Run kotlinc with the normal Kotlin sources and Kotlin sources generated in step 2 4. Run javac with Java sources and the compiled Kotlin classes
generate stubs
Run apt
kotlinc
javac
stubs
kotlin sources
compiled classes
kotlinc with kapt
generate stubs
Run apt
kotlinc
javac
stubs
kotlin sources
compiled classes
kaptGenerateStubsDebugKotlin
kaptDebugKotlin
compileDebugKotlin
compileDebugJavaWithJavac
Gradle tasks
🐢 extensive symbol resolution 🐢
KSP
kotlinc
javac
kotlin sources
compiled classes
kspDebugKotlin
compileDebugKotlin
compileDebugJavaWithJavac
Gradle tasks for KSP
judicious symbol resolution 🧠
kotlin sources
Symbol resolution? 🤔
Part 3: The Old Man and the IDE
Part 4: The Great KatSPy
Multiple rounds
val builder = AClassBuilder() // generated
val a = builder
.withA(1)
.withB("foo")
.withC(2.3)
.build()
package com.example
import com.example.annotation.Builder
@Builder
class AClass(private val a: Int, val b: String, val c: Double) {
val p = "$a, $b, $c"
fun foo() = p
}
Builder
Processor
Hello
Processor
// HELLO.kt
class HELLO {
// some content
}
package com.example
import com.example.annotation.Builder
import HELLO
@Builder
class AClass(private val a: Int, val b: String, val c: Double, val d: HELLO) {
val p = "$a, $b, $c, ${d.foo()}"
fun foo() = p
}
Defer creating a builder until another processor has provided HELLO
Unresolved reference: HELLO
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.annotation.Builder")
val deferred = symbols.filter { !it.validate() }
symbols
.filter { it is KSClassDeclaration && it.validate() }
.map { it.accept(BuilderVisitor(), Unit) }
return deferred
}
Defer processing symbols that aren't valid
Builder
Hello
defer AClass.kt
HELLO.kt
Round 1
Round 2
Builder
AClassBuilder.kt
Builder
Hello
no outputs
no outputs
Termination condition
other rounds
Any deferred symbols left over will be logged as an error ❌
Terminate when no outputs from any processor 🛑
Incremental processing
FileSpec.builder(
it.classDeclaration.packageName.asString(),
it.classDeclaration.simpleName.asString() + "Ext"
).addFunction(sumIntsFunction)
.build()
.writeTo(codeGenerator, aggregating = false, extraOriginatingKSFiles = resolvedFiles)
🤔
A simple processor
// AExt.kt
fun A.fascinating() = println("fascinating!")
// A.kt
@Interesting
class A
// AExt.kt
fun A.fascinating() = println("fascinating!")
// BExt.kt
fun B.fascinating() = println("fascinating!")
// A.kt
@Interesting
class A
// B.kt
@Interesting
class B
What happens to output AExt.kt if we add a new source? 🤔
other sources
// AExt.kt
fun A.fascinating() = println("fascinating!")
// A.kt
@Interesting
class A
// B.kt
@Interesting
class B
// C.kt
@Interesting
class C
🛡️ AExt.kt is protected from having to change due to a new source
// D.kt
class D
// E.kt
class E
It is therefore an "isolating" output
// A.kt
@Interesting
class A
// B.kt
@Interesting
class B
// C.kt
@Interesting
class C
//Fascinating.kt
class Fascinating(
a: A,
b: B,
c: C,
)
A different processor
collecting
// _A.kt
@Interesting
class _A
//Fascinating.kt
class Fascinating(
_a: _A,
a: A,
b: B,
c: C,
)
New source requires rebuilding output
What happens to output Fascinating.kt if we add a new source? 🤔
Fascinating.kt is an "aggregating" output
// A.kt
@Interesting
class A
// B.kt
@Interesting
class B
// C.kt
@Interesting
class C
//Fascinating.kt
class Fascinating(
a: A,
b: B,
c: C,
)
// A.kt
@Interesting
class A
// B.kt
@Interesting
class B
// C.kt
@Interesting
class C
//Fascinating.kt
class Fascinating(
a: A,
b: B,
c: C,
)
// D.kt
class D
Can I change D.kt in a way that would invalidate Fascinating.kt? 🤔
//Fascinating.kt
class Fascinating(
a: A,
b: B,
c: C,
d: D, // change from adding @Interesting
)
Output needs a rebuild. It's not isolated from changes outside registered inputs
D.kt isn't registered as an input for Fascinating.kt
// A.kt
@Interesting
class A
// B.kt
@Interesting
class B
// C.kt
@Interesting
class C
//Fascinating.kt
class Fascinating(
a: A,
b: B,
c: C,
)
// D.kt
class D
// E.kt
class E
Does deleting these classes cause reprocessing of A, B, C? 🤔
👍 Unrelated classes can be deleted safely
This is the only kind of change that doesn't require rebuilding aggregating outputs.
To summarize, if an output might depend on new or any changed sources, it is considered aggregating. Otherwise, the output is isolating.
XProcessing
consumes
How to migrate the processor to KSP? 🤔
interface XElement {
val name: String
val packageName: String
val enclosingElement: XElement?
fun isPublic(): Boolean
fun isProtected(): Boolean
fun isAbstract(): Boolean
fun isPrivate(): Boolean
fun isStatic(): Boolean
fun isTransient(): Boolean
fun isFinal(): Boolean
fun kindName(): String
fun asTypeElement() = this as XTypeElement
fun asDeclaredType(): XDeclaredType {
return asTypeElement().type
}
}
Declare abstraction over javax Element
An idea from @yigitboyar
XProcessing PRs
I'm David 👋
@d_rawers
Image credits
Photo | Link | Author link | Author name |
---|---|---|---|
University of Chicago | https://unsplash.com/photos/ba8bUAKjYWg | https://unsplash.com/@alisaanton | Alisa Anton |
A Moveable Feast (cover) | https://en.wikipedia.org/wiki/File:MoveableFeast.jpg | ||
Two Roads | https://unsplash.com/photos/u0vgcIOQG08 | https://unsplash.com/@madebyjens | Jens Lelie |
Bullet | https://unsplash.com/photos/5HBpbWsdpck | https://unsplash.com/@jhphotos04 | Joseph Hersh |
A Farewell To Arms (cover) | https://www.flickr.com/photos/digitalcollectionsum/15124278710 | ||
The Old Man And The Sea (cover) | https://www.abebooks.com/servlet/BookDetailsPL?bi=30174085940 | ||
The Great Gatsby (cover) | https://www.abebooks.com/GREAT-GATSBY-FitzGerald-F-Scott-Bantam/30461427379/bd | ||
NZ flag | https://unsplash.com/photos/Afs-F8pRIeE | https://unsplash.com/@claudettewicks | Claudette Wicks |
Codegen with KSP. A Farewell to Stubs
By David Rawson
Codegen with KSP. A Farewell to Stubs
- 388