たくさんの自由帳
Androidのお話
たくさんの自由帳
文字数(だいたい) : 5215
どうもこんにちは。Windows 11にするときにAndroid Studioも入れなおしたんですが、何の設定をしてたか忘れちゃってその都度直してたんですが、たぶん思い出せたかも。自分用にそのうち書きます。
コードを動的にいじりたくて、KSPを調べてたんだけど、結局要件を満たせなさそうで使わないかも。それでKSPの使い方だけでもメモ。
ことりん しんぼる ぷろせっしんぐ
アノテーションをつけたKotlinコードに対してビルド時にアクセスできる。この時に追加でコードを生成したりできる。
これだけ見るとめっちゃ難しそうに見えるが、KSPはそんなに難しくなかった。Pluginの方はたぶんつらい。
KSPのがずっと簡単なのだが、既存のコードを書き換える機能とかは持ち合わせてない。
AndroidのRoomは@Queryに渡したSQLを元に実際にContentValuesでSQLiteに問い合わせるコードをKSPで作っているらしい。
というわけで今回は簡単にコードを生成してみようと思います。思ったよりも簡単だった。
といっても特には思いつかなかったので、振り絞って考えてみました。
任意のUI Stateのdata classを元に、Loading、Success、Errorを自動生成してみようかと。もっといい例が欲しかった。
// これから
@GenerateUiState // 生成してほしいので
data class DetailScreen(
val name: String,
val description: String
)// これを作りたい
sealed interface GenerateDetailScreen {
data object Loading: GenerateDetailScreen
data class Success(
val name: String,
val description: String
) : GenerateDetailScreen
data class Error(val throwable: Throwable): GenerateDetailScreen
}KSP作るのにIDEAを使います。Android Studioでできるかは分からないです
| なまえ | あたい |
|---|---|
| IDEA | IntelliJ IDEA 2025.3.1.1 |
| Kotlin | 2.3.0 |
| Gradle | 9.2.1 |
| JDK | 25 (私は Temurin 派) |
Java 25でも、Gradle 9.2.1とKotlin 2.3.0の組み合わせなら動きました。両方25に対応してた。
Kotlinプロジェクトを作るアノテーションを保持するモジュール、KSPの処理をするモジュールを作るKSP書くモジュールを作って動かしてみる適当に作ってください
srcは使わない予定なので消しちゃう。exampleを別に作ります。
annotationモジュールはその名の通りアノテーションを定義するための。processorがKSPでやりたいことを書くモジュールになります。
できた!
あと、パッケージ通りにフォルダを作ってくれないので自分で作りました。KSPを試す分には関係ない部分ですが、、、
最初かあるMain.ktとかは使わないので消して、annotationパッケージにアノテーションを置く.ktファイルを作りました。GenerateUiState.kt
@Target(AnnotationTarget.CLASS)
annotation class GenerateUiStateこれだけです。つぎはKSP側を
processorフォルダのbuild.gradle.ktsに、さっき作ったアノテーションとksp開発用のライブラリを入れます。KSPはKotlinのバージョンに対応したのをいれてね。
dependencies {
// 省略...
implementation(project(":annotation")) // アノテーションを取り込む
implementation("com.google.devtools.ksp:symbol-processing-api:2.3.4") // ksp も取り込む
}GenerateUiStateProvider.ktを作りました。あとでGenerateUiStateProcessorとresources フォルダの方もやりますが
まずはProviderの方ですが、SymbolProcessorProviderを実装したクラスを作ります。create関数を実装するように言われます。ここでProcessorを返すわけですが、この後作るのでエラーになっても仕方ない。
class GenerateUiStateProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return GenerateUiStateProcessor(
options = environment.options,
logger = environment.logger,
codeGenerator = environment.codeGenerator
)
}
}これらの引数ですが、optionsはなんかbuild.gradle.ktsから指定したものを受け取れるらしいです。loggerはわからず、codeGeneratorはその名の通りKotlinコードを自動生成する際に使います。
とりあえずGenerateUiStateProcessorを作りますか。後で中身を埋めていきます。
忘れないうちに次にやることは、resourcesフォルダにファイルを置くことです。resourcesフォルダにMETA-INFフォルダを作り、その中にservicesフォルダを作り、その中にcom.google.devtools.ksp.processing.SymbolProcessorProviderという名前のテキストファイルを作ります。
ファイルの中身ですが、GenerateUiStateProviderの名前を完全修飾名で書くだけです。パッケージ名+クラス名
入力が面倒な場合はCtrl + Space(Windowsの場合)でコード補完を呼び出して、一つだけ候補が表示されるのでそれを入力すればよいです。
GenerateUiStateProcessorクラスを作ったらSymbolProcessorを実装するようにします。process関数を実装するように言われます。ここでアノテーションが付いたKotlinコードへアクセスできるという魂胆であります。
class GenerateUiStateProcessor(
options: Map<String, String>,
logger: KSPLogger,
private val codeGenerator: CodeGenerator
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
// TODO このあとすぐ
}
}じゃあ書きますって言われてもわからんと思うので、とりあえず 冒頭通りUiState を生成するコードを先に貼ります。
解説はこの後すぐ
class GenerateUiStateProcessor(
options: Map<String, String>,
logger: KSPLogger,
private val codeGenerator: CodeGenerator
) : SymbolProcessor {
/** ファイルが二回作られるのを防ぐ */
private var invoked = false
override fun process(resolver: Resolver): List<KSAnnotated> {
// アノテーションがついているクラスを探す
val symbols = resolver
.getSymbolsWithAnnotation(GenerateUiState::class.qualifiedName!!) // implementation(project(":annotation")) したのでアクセス可。名前をべた書きしてもよかったかも・・・
.filterIsInstance<KSClassDeclaration>() // クラスに限定する、Function とかもあります
if (!invoked) {
invoked = true
// ファイルを作る
symbols.forEach { symbol ->
// 自動生成するクラス名
val generatedClassName = "Generated${symbol.qualifiedName!!.getShortName()}"
// クラスの引数を val hoge: String にする
val parameterKotlinCode = symbol
.getAllProperties()
.joinToString(separator = ", ") {
"val ${it.simpleName.getShortName()}: ${it.type.resolve().declaration.qualifiedName!!.asString()}"
}
// ファイルを作る
// ビルドすると IDEA / AndroidStudio から参照可能になる
val file = codeGenerator.createNewFile(
dependencies = Dependencies(false, *resolver.getAllFiles().toList().toTypedArray()),
packageName = "io.github.takusan23.kspuistategenerator",
fileName = generatedClassName
)
// 自動生成する Kotlin コード
val generatedKotlinCode = """
package io.github.takusan23.kspuistategenerator
sealed interface $generatedClassName {
data object Loading : $generatedClassName
data class Success(
$parameterKotlinCode
) : $generatedClassName
data class Error(val throwable: Throwable) : $generatedClassName
}
""".trimIndent()
// 書き込む
file.write(generatedKotlinCode.toByteArray())
}
}
val unableToProcess = symbols.filterNot { it.validate() }.toList()
return unableToProcess
}
}まああんまりよくわかってないんですが
// アノテーションがついているクラスを探す
val symbols = resolver
.getSymbolsWithAnnotation(GenerateUiState::class.qualifiedName!!) // implementation(project(":annotation")) したのでアクセス可。名前をべた書きしてもよかったかも・・・
.filterIsInstance<KSClassDeclaration>() // クラスに限定する、Function とかもありますsymbolの中身はまあfilterしたのでKSClassDeclarationになるわけです。
ここに、どのクラスに対してアノテーションを付けたか。みたいな情報が得られます。SequenceなのでtoList()した方がデバッグは簡単。
@GenerateUiState data class HomeState // この場合は
println(symbols.map { it.qualifiedName!!.asString() }.toList()) // [io.github.takusan23.io.github.takusan23.kspuistategenerator.example.HomeState] こうなるクラスが取れたので次はクラスのパラメーターを解析します。これから自動生成するクラスの引数になるので!getAllProperties()でとれます。
val parameterKotlinCode = symbol
.getAllProperties()
.joinToString(separator = ", ") {
"val ${it.simpleName.getShortName()}: ${it.type.resolve().declaration.qualifiedName!!.asString()}"
}これも実行するとこうなります。
// これを渡すと
@GenerateUiState
data class HomeState(
val title: String,
val description: String
)
println(parameterKotlinCode) // こうなる→ val title: kotlin.String, val description: kotlin.StringそしてあとはKotlinのコードを文字列リテラルの中に書いて保存して終わり。なんと文字列で書くことができます。
// ファイルを作る
// ビルドすると IDEA / AndroidStudio から参照可能になる
val file = codeGenerator.createNewFile(
dependencies = Dependencies(false, *resolver.getAllFiles().toList().toTypedArray()),
packageName = "io.github.takusan23.kspuistategenerator",
fileName = generatedClassName
)
// 自動生成する Kotlin コード
val generatedKotlinCode = """
package io.github.takusan23.kspuistategenerator
sealed interface $generatedClassName {
data object Loading : $generatedClassName
data class Success(
$parameterKotlinCode
) : $generatedClassName
data class Error(val throwable: Throwable) : $generatedClassName
}
""".trimIndent()
// 書き込む
file.write(generatedKotlinCode.toByteArray())if (!invoked) { }の分岐はここのファイル生成が二回目に対応していないために必要だったわけですね。なんで二回目があるのかはわからないですが・・・
サンプル用のモジュールを作りましょう。exampleって名前にしました。
できたら、build.gradle.ktsへkspライブラリを入れ、annotationモジュールと、ksp()としてprocessorモジュールを読み込むようにします。
ここからはRoom入れるのと同じ流れです。
plugins {
kotlin("jvm")
id("com.google.devtools.ksp") version "2.3.3" // KSP 入れる
}
group = "io.github.takusan23.kspuistategenerator.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
// アノテーションと ksp を入れる
implementation(project(":annotation"))
ksp(project(":processor"))
}
tasks.test {
useJUnitPlatform()
}あとは適当にMain.ktとかでアノテーションを付けたクラスを作り、たぶん一度実行する必要があります。fun main()の横にある実行ボタンを押して実行すればよいです。
@GenerateUiState
data class DetailScreen(
val name: String,
val description: String
)
fun main() {
}できた!
生成したクラスはプロジェクト/モジュール名/build/generated/ksp/main/kotlin/パッケージ名/KSPで作ったファイル名にあります!
Execution failed for task ':example:kspKotlin'.
> A failure occurred while executing com.google.devtools.ksp.gradle.KspAAWorkerAction
> com/google/devtools/ksp/impl/KotlinSymbolProcessing$ExitCoderesources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProviderのパスが間違えてるとか
中身はCtrl + Spaceの補完で表示される一つだけのやつを入力すればよいはず
残念ながらKSPが実行される環境では自分の作ったクラスとかは参照できない模様。はえ~
annotation class ClassArg(val clazz: KClass<*>) // KSP ではクラスが存在しないエラーになってしまう
class ExampleB
@ClassArg(ExampleB::class) // 引数に渡しても KSP からは見つけることができない
class ExampleAどぞ
ほかのプロジェクトで使う場合、手っ取り早いのはmavenLocalに公開することだと思います、が、ほかの人がビルドできなくなってしまう。