Search code examples
androidkotlingradleopenapiopenapi-generator

OpenApi Client Kotlin Jar Dependency build with Gradle is not recognized in my Android Kotlin Jetpack Compose Project


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 !


Solution

  • 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) :

    Project Structure

    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