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

  • 394