目次

02.KSPプロセッサープラグインの作成









Roomのデータベース定義では

@Database(
  entities = [Settings::class],
  version = 1,
  exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
  abstract fun SettingsDao(): SettingsDao
   :
}

のように、entitiesでエンティティ、AppDatabase内でDAO列挙します。

エンティティやDAOの追加、削除等による列挙ミスを防止するため、列挙するエンティティやDAOを自動生成するKSPプロセッサープラグインを作成します。

KSPライブラリの導入

エンティティやDAOの自動生成に必要なKSPライブラリをバージョンカタログに追記します。

[versions]
   :
# ./gradlew -versionで表示されるKotlinのバージョンに適した最新バージョンにします(自動で上げない)
#noinspection GradleDependency,NewerVersionAvailable
gradle-ksp = "2.0.21-1.0.28"        # https://mvnrepository.com/artifact/com.google.devtools.ksp/symbol-processing-api
kotlin-poet = "2.2.0"               # https://mvnrepository.com/artifact/com.squareup/kotlinpoet
   :
[libraries]
   :
gradle-ksp  = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "gradle-ksp" }
kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlin-poet"  }
kotlin-poet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin-poet"  }
   :
room-common = { module = "androidx.room:room-common", version.ref = "room" }
   :

5行目
コメント部分のURLを参照して、gradleKotlinに合った最新安定バージョンを指定します。
6行目
コメント部分のURLを参照して、最新安定バージョンを指定します。
10行目~12行目
エンティティやDAOの自動生成に必要なライブラリを追記します。
14行目
Roomの型参照用にライブラリを追記します。

追記後、『Sync Now』で内容をプロジェクトに反映させます。

KSPプロセッサーモジュールの作成

KSPプロセッサーモジュール:core:database:kspAndroid Studioモジュール作成ウィザードを使わずにモジュールを作成します。

/core/database直下にkspディレクトリを追加し、その直下にkotlinスクリプトbuild.gradle.ktsを作成してモジュール化します。

plugins {
  alias(libs.plugins.kotlin.jvm)
}

java {
  toolchain {
    languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get().toInt()))
  }
}

dependencies {
  implementation(libs.gradle.ksp)
  implementation(libs.kotlin.poet)
  implementation(libs.kotlin.poet.ksp)
  implementation(libs.room.common)
}

5行目~6行目
ビルドモジュールへの依存をMavenライブラリ形式で行うための定義をしています。
9行目~11行目
エンティティコレクターに必要な依存ライブラリを指定しています。

モジュール作成ウィザードを使わずにモジュールを作成したため、ビルド対象モジュールに手動で追記します。

   :
include(":core:database")
include(":core:database:ksp")
   :

3行目
ビルド対象モジュールに追記します。

追記後、『Sync Now』で内容をプロジェクトに反映させます。

エンティティコレクターの作成

:core:database:kspモジュールにsrc/main/kotlinディレクトリを作成し、jp.co.progress_llc.portal.core.database.ksp.modelパッケージ(ディレクトリ)を追加します。

作成したパッケージ内にエンティティコレクター用のデータモデルEntityModel.ktを作成します。

package jp.co.progress_llc.portal.core.database.ksp.model

data class EntityModel(
  val packageName: String,
  val simpleName:  String
) {
  val qualifiedName: String = "$packageName.$simpleName"
}

4行目
エンティティのパッケージ名です。
5行目
エンティティのクラス名です。
7行目
エンティティの完全修飾名です。

modelパッケージと同階層にcollectorパッケージ(ディレクトリ)を追加します。

作成したパッケージ内にエンティティコレクターEntityCollector.ktを作成します。

package jp.co.progress_llc.portal.core.database.ksp.collector

import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSClassDeclaration
import jp.co.progress_llc.portal.core.database.ksp.model.EntityModel

class EntityCollector(
  private val resolver: Resolver,
  private val logger:   KSPLogger
) {
  fun collect(): List<EntityModel> {
    return resolver
      .getSymbolsWithAnnotation("androidx.room.Entity")
      .filterIsInstance<KSClassDeclaration>()
      .map { entity ->
        EntityModel(
          packageName = entity.packageName.asString(),
          simpleName  = entity.simpleName.asString()
        )
      }
      .toList()
  }
}

14行目
Roomのエンティティを取得します。
15行目
データクラスに絞り込みます。
16行目~21行目
エンティティモデルのマップを生成します。
22行目
マップをリストに変換します。

DAOコレクターの作成

modelパッケージ内にDAOコレクター用のデータモデルDaoModel.ktを作成します。

package jp.co.progress_llc.portal.core.database.ksp.model

data class DaoModel(
  val packageName: String,
  val simpleName:  String
) {
  val qualifiedName: String = "$packageName.$simpleName"
  val functionName:  String = simpleName.replaceFirstChar { it.lowercaseChar() }
}

4行目
DAOのパッケージ名です。
5行目
DAOのクラス名です。
7行目
DAOの完全修飾名です。
8行目
DAOの関数名(SettingsDaosettingsDao)です。

collectorパッケージ内ににDAOコレクターDaoCollector.ktを作成します。

package jp.co.progress_llc.portal.core.database.ksp.collector

import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSClassDeclaration
import jp.co.progress_llc.portal.core.database.ksp.model.DaoModel

class DaoCollector (
  private val resolver: Resolver,
  private val logger:   KSPLogger
) {
  fun collect(): List<DaoModel> {
    return resolver
      .getSymbolsWithAnnotation("androidx.room.Dao")
      .filterIsInstance<KSClassDeclaration>()
      .map { dao ->
        DaoModel(
          packageName = dao.packageName.asString(),
          simpleName  = dao.simpleName.asString()
        )
      }
      .toList()
  }
}

14行目
RoomDaoを取得します。
15行目
インターフェースクラスに絞り込みます。
16行目~21行目
DAOモデルのマップを生成します。
22行目
マップをリストに変換します。

AppDatabaseGeneratorの作成

collectorパッケージと同階層にgeneratorパッケージ(ディレクトリ)を追加します。

追加したパッケージ内にRoomデータベースを生成するkotlinクラスファイルAppDatabase.ktを自動生成するAppDatabaseGenerator.ktを作成します。

package jp.co.progress_llc.portal.core.database.ksp.generator

import com.google.devtools.ksp.processing.CodeGenerator
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.writeTo
import jp.co.progress_llc.portal.core.database.ksp.model.EntityModel
import jp.co.progress_llc.portal.core.database.ksp.model.DaoModel

class AppDatabaseGenerator (
  private val codeGenerator: CodeGenerator,
  private val entities: List<EntityModel>,
  private val daos:     List<DaoModel>
) {
  fun generate() {
    val fileSpec = FileSpec.builder(
      packageName = "jp.co.progress_llc.portal.core.database",
      fileName = "AppDatabase"
    )
      .addType(createDatabaseType())
      .build()
    fileSpec.writeTo(codeGenerator, aggregating = true)
  }

  private fun createDatabaseType(): TypeSpec {
    return TypeSpec.classBuilder("AppDatabase")
      .addModifiers(KModifier.ABSTRACT)
      .addAnnotation(createDatabaseAnnotation())
      .superclass(
        ClassName("androidx.room", "RoomDatabase")
      )
      .addFunctions(createDaoFunctions())
      .build()
  }

  private fun createDatabaseAnnotation(): AnnotationSpec {
    val databaseClass = ClassName("androidx.room", "Database")
    val entitiesCode = entities.joinToString(",\n") {
      "%T::class"
    }
    val entityClassNames = entities.map {
      ClassName(it.packageName, it.simpleName)
    }
    return AnnotationSpec.builder(databaseClass)
      .addMember(
        "entities = [%L]",
        CodeBlock.builder()
          .add(
            entitiesCode,
            *entityClassNames.toTypedArray()
          )
          .build()
      )
      .addMember("version = %L", 1)
      .addMember("exportSchema = %L", false)
      .build()
  }

  private fun createDaoFunctions(): List<FunSpec> {
    return daos.map { dao ->
      FunSpec.builder(dao.functionName)
        .addModifiers(KModifier.ABSTRACT)
        .returns(
          ClassName(dao.packageName, dao.simpleName)
        )
        .build()
    }
  }
}

16行目
生成するAppDatabaseのパッケージ名です。
26行目
生成するクラスをabstractにします。
27行目
生成するクラスを@Databaseを付加します。
31行目
生成するクラスにfunを付加します。
35行目~56行目
@Databaseアノテーションを生成します。
58行目~67行目
DAOを呼び出すfunを生成します。

KSPプロセッサーの作成

AppDatabaseGeneratorを呼び出すKSPプロセッサーDatabaseProcessor.ktを作成します。

package jp.co.progress_llc.portal.core.database.ksp

import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSAnnotated
import jp.co.progress_llc.portal.core.database.ksp.collector.EntityCollector
import jp.co.progress_llc.portal.core.database.ksp.collector.DaoCollector
import jp.co.progress_llc.portal.core.database.ksp.generator.AppDatabaseGenerator

class DatabaseProcessor(
  private val env: SymbolProcessorEnvironment
) : SymbolProcessor {
  private val logger        = env.logger
  private val codeGenerator = env.codeGenerator
  override fun process(resolver: Resolver): List<KSAnnotated> {
    val entities = EntityCollector(resolver, logger).collect()
    val daos     = DaoCollector(resolver, logger).collect()
    if (entities.isEmpty()) {
      return emptyList()
    }
    logger.warn(" ${entities.size} @Entity found.")
    AppDatabaseGenerator(
      codeGenerator = codeGenerator,
      entities = entities,
      daos = daos
    ).generate()
    return emptyList()
  }
}

17行目
エンティティ一覧を取得します。
18行目
DAO一覧を取得します。
23行目~27行目
AppDatabaseを自動生成します。

KSPに提供

DatabaseProcessorKSPに提供するプロバイダーDatabaseProcessorProvider.ktを作成します。

package jp.co.progress_llc.portal.core.database.ksp

import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider

class DatabaseProcessorProvider : SymbolProcessorProvider {
  override fun create(
    environment: SymbolProcessorEnvironment
  ): SymbolProcessor {
    return DatabaseProcessor(environment)
  }
}

11行目
DatabaseProcessorを呼び出しています。

サービス登録

プロバイダーをKSPのサービスに登録して実行されるようにします。

/src/mainセットにresources/META-INF/servicesディレクトリを追加し、直下にcom.google.devtools.ksp.processing.SymbolProcessorProviderを作成します。

jp.co.progress_llc.portal.core.database.ksp.DatabaseProcessorProvider

1行目
DatabaseProcessorProviderKSPのサービスに登録します。

プラグインの作成

:build-logicモジュールにKspDatabaseプラグインKspDatabasePlugin.ktを作成します。

package jp.co.progress_llc.portal.buildlogic.plugins

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies

class KspDatabasePlugin : Plugin<Project> {
  override fun apply(project: Project) = with(project) {
    apply(plugin = "com.google.devtools.ksp")
    dependencies {
      add("ksp", project(":core:database:ksp"))
    }
  }
}

10行目
KSPプラグインを登録します。
12行目
KSPプロセッサーモジュールをKSPの依存ライブラリ登録します。

プラグインの登録

:build-logicモジュールのbuild.gradle.ktsKspDatabaseプラグインを追記します。

   :
gradlePlugin {
   :
  plugins {
    register("KspDatabasePlugin") {
      id = "build.logic.ksp.database"
      implementationClass = "jp.co.progress_llc.portal.buildlogic.plugins.KspDatabasePlugin"
    }
  }
   :

6行目
プラグインを呼び出すidを定義します。
7行目
プラグインのクラス名をパッケージ名とともに定義します。

追記後、『Sync Now』で内容をプロジェクトに反映させます。

プラグインの適用

Roomデータベースを定義している:core:databaseモジュールにKspDatabaseプラグインを適用します。

plugins {
   :
  id("build.logic.ksp.database")
}
   :

3行目
KspDatabaseプラグインのidを追記します。

追記後、『Sync Now』で内容をプロジェクトに反映させます。

./gradlew clean :core:database:build
を実行してbuild/generated/ksp/release/kotlin/jp/co/progress_llc/portal/core/databaseAppDatabase.ktが作成されていることを確認します。