I'm actually working on a project with a backend in Kotlin Springboot with OpenApi generated Api (in yaml) that i want to plug with a client Android Jetpack Compose.
What I want to do is to generate from my backend project the openApi client as a Jar Kotlin dependency and import it in my client project.
Here's the backend gradle build to achieve the phase 'client generation / jar packaging / maven local publish' :
Backend Client Generation :
tasks.register("generateKotlinClient", org.openapitools.generator.gradle.plugin.tasks.GenerateTask::class) {
generatorName.set("kotlin")
inputSpec.set("$rootDir/src/main/resources/api/api.yaml")
outputDir.set(layout.buildDirectory.dir("generated/grimoire-server-client").get().asFile.path)
invokerPackage.set("com.com.corbz.grimoire.backend.client")
packageName.set("com.com.corbz.grimoire.backend.client")
apiPackage.set("com.com.corbz.grimoire.backend.client.api")
modelPackage.set("com.com.corbz.grimoire.backend.client.model")
generateApiDocumentation.set(false)
generateModelDocumentation.set(false)
generateApiTests.set(false)
generateModelTests.set(false)
configOptions.set(
mapOf(
"sourceFolder" to "."
)
)
}
tasks.register("packageClientJar", Jar::class) {
group = "build"
val generatedClientDir = layout.buildDirectory.dir("generated/grimoire-server-client/com").get().asFile
from(generatedClientDir)
archiveBaseName.set("grimoire-backend-client")
archiveVersion.set("1.0.0")
destinationDirectory.set(layout.buildDirectory.dir("libs").get().asFile)
}
publishing {
publications {
create<MavenPublication>("grimoireBackendClient") {
groupId = "com.corbz"
artifactId = "grimoire-backend-client"
version = "1.0.0"
artifact(tasks.named("packageClientJar").get()) {
builtBy(tasks.named("packageClientJar"))
}
}
}
repositories {
maven {
name = "local"
url = uri("${System.getProperty("user.home")}/.m2/repository")
}
}
}
It generates me a jar with a package folder and a manifest folder. My Android project is organized with a main build.gradle.kts / setting.gradle.kts and my code is on a app package with his own build.gradle.kts /setting.gradle.kts. In the build.gradle.kts from app package my library is imported like this (i didn't put all the file with all the dependencies but that's how i import the client dependency) :
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.corbz.grmoire.android"
compileSdk = 35
defaultConfig {
applicationId = "com.corbz.grmoire.android"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
}
dependencies {
// Business dependencies --------------------------
implementation(libs.grimoire.backend.client)
implementation(libs.moshi)
implementation(libs.moshi.kotlin)
// ------------------------------------------------
}
For a reason i do not understand the library is recognized, i can import the package like this :
package com.corbz.grmoire.android.config
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import com.corbz.grimoire.backend.client.model.*
@HiltAndroidApp
class GrimoireApplication: Application() {
}
But if i try to import for exemple a GrimoireDto from this package, it is not recognized. Here's the generated class (kt) for my GrimoireDto from the openApi client dependency :
@file:Suppress(
"ArrayInDataClass",
"EnumEntryName",
"RemoveRedundantQualifierName",
"UnusedImport"
)
package com.com.corbz.grimoire.backend.client.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
*
*
* @param code Unique identifier for the grimoire.
* @param creationDate The creation date of the grimoire.
* @param lastUpdateDate The last update date of the grimoire.
*/
data class GrimoireDto (
/* Unique identifier for the grimoire. */
@Json(name = "code")
val code: kotlin.String,
/* The creation date of the grimoire. */
@Json(name = "creationDate")
val creationDate: java.time.OffsetDateTime,
/* The last update date of the grimoire. */
@Json(name = "lastUpdateDate")
val lastUpdateDate: java.time.OffsetDateTime
) {
}
I don't understand why this Kotlin class can't be import since it's a .kt file, there's nothing wrong in the class for me, i also tried to import sub dependencies (like mochi for the client) to have all dependencies working but nothing worked. I even try to generate with the android generator (for me it should work with kotlin multiplatform but it was 'for the science') but the problem is still the same.
Could you help to figure it out what's wrong on my generation / import? I would like to import this client to avoid manual coding of client-side call to backend.
Thanks in advance folks !
After some searches and tries, i figured it out to solve my problems. What i've done, as it is suggested in some cases, is that I changed my project structure to have a root build, and separate the app module (with sources, build definition, tests) and the client module (client build and jar packaging) :
Here's the client build.gradle.kts :
plugins {
// Inherited
kotlin("jvm")
id("org.openapi.generator")
id("maven-publish")
}
group = "com.corbz.grimoire"
version = "0.0.2-SNAPSHOT"
repositories {
mavenCentral()
}
sourceSets {
main {
java.srcDir("src/main/generated-client")
}
}
/**
* Build Processing Workflow
*/
task("processGeneratedFilesClient") {
val generatedDir = file("$projectDir/src/main/generated-client")
println("GeneratedDir to clean : $generatedDir")
if (generatedDir.exists()) {
println("Cleaning up existing generated files...")
delete(generatedDir)
}
}
// Ajout d'une tâche pour s'assurer que les sources sont générées avant la compilation
tasks.compileKotlin {
dependsOn(tasks.openApiGenerate)
}
// Backend Client Generation
openApiGenerate {
generatorName.set("kotlin")
inputSpec.set("$rootDir/api/api.yaml") // Chemin vers ton fichier YAML
outputDir.set("$projectDir/src/main/generated-client")
invokerPackage.set("com.corbz.grimoire.backend.client")
packageName.set("com.corbz.grimoire.backend.client")
apiPackage.set("com.corbz.grimoire.backend.client.api")
modelPackage.set("com.corbz.grimoire.backend.client.model")
generateApiDocumentation.set(false)
generateModelDocumentation.set(false)
generateApiTests.set(false)
generateModelTests.set(false)
configOptions.set(mapOf("sourceFolder" to ""))
}
publishing {
publications {
create<MavenPublication>("grimoireBackendClientJar") {
groupId = project.group.toString()
artifactId = "grimoire-backend-client"
version = project.version.toString()
from(components["java"])
}
}
repositories {
mavenLocal()
}
}
dependencies {
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
And here's the app build :
plugins {
kotlin("plugin.spring") version "2.1.0"
id("org.springframework.boot") version "3.3.5"
id("io.spring.dependency-management") version "1.1.6"
// Inherited
kotlin("jvm")
id("org.openapi.generator")
}
group = "com.corbz.grimoire.backend.app"
version = "0.0.1-SNAPSHOT"
kotlin {
jvmToolchain(23)
}
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
// Open Api Generation
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springdoc:springdoc-openapi-starter-common:2.2.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Spring Ecosystem
implementation("org.springframework.boot:spring-boot-starter-web")
// database
implementation("org.springframework.boot:spring-boot-starter-data-mongodb") // Documents / Consistences negligeables
implementation("org.springframework.boot:spring-boot-starter-data-jpa") // Utilisateurs / Consistences fortes
implementation("org.postgresql:postgresql:42.7.4")
implementation("de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring25x:4.18.0") //TODO : Pas sûr d'en avoir besoin
implementation("org.jetbrains.kotlin:kotlin-reflect") //TODO: idem
// Security
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
// Unit testing
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
// Integration testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:testcontainers:1.17.6")
testImplementation("org.testcontainers:postgresql:1.20.4")
testImplementation("org.testcontainers:mongodb:1.17.6")
testImplementation("org.testcontainers:junit-jupiter:1.17.6")
}
sourceSets {
main {
java.srcDir("src/main/generated-api") // Ajouter le dossier 'generated' comme répertoire source
}
}
/**
* Build Processing Workflow
*/
task("processGeneratedFilesApi") {
// Nettoyer le dossier 'generated' s'il existe
val generatedDir = file("$projectDir/src/main/generated-api")
println("GeneratedDir to clean : $generatedDir")
if (generatedDir.exists()) {
println("Cleaning up existing generated $projectDir files...")
delete(generatedDir) // Supprimer les fichiers existants dans le dossier 'generated'
}
}
tasks.compileKotlin {
dependsOn(tasks.openApiGenerate)
}
/**
* API Generation
*/
//Server Api Generation
openApiGenerate {
generatorName.set("kotlin-spring")
inputSpec.set("$rootDir/api/api.yaml") // Chemin vers ton fichier YAML
outputDir.set("$projectDir/src/main/generated-api") // Dossier où tu veux que le code généré soit placé
apiPackage.set("com.corbz.grimoire.api")
modelPackage.set("com.corbz.grimoire.api.model")
configOptions.set(
mapOf(
"interfaceOnly" to "true",
"skipDefaultInterface" to "true", // Force l'implémentation des APIs
"useSpringBoot3" to "true", // Utilisation de jakarta vs javax,
"sourceFolder" to "", // Place le package au bon niveau,
"gradleBuildFile" to "false",
"documentationProvider" to "none"
)
)
}
/**
* Testing tasks
*/
tasks.withType<Test> {
useJUnitPlatform()
}
That's allows me to build the client as a jar dependencies to be integrate in other project as dependency