I tried to fix the issues related to Ktorfit used in multiple modules and saw that there is an issue with how Ktorfit currently works and i don`t know a good way to fix this. If you have any ideas, let me know.
I will explain in short how Ktorfit currently works
How Ktorfit works:
package com.example
import com.example.model.People
import de.jensklingenberg.ktorfit.http.GET
interface ExampleApi {
@GET("/test")
suspend fun exampleGet(): People
}
Let`s say we a interface like this.
At compile time Ktorfit/KSP checks for all functions that are annotated with Ktorfit annotations like @get.
Then it looks at the parent interfaces of that functions and generates, the source code of a Kotlin class that implements the interface. The classes are named like the interfaces but with an underscore at the beginning and "Impl" at the end and they have the same package as the interfaces. In this case a class named _ExampleApiImpl will be generated.
public class _ExampleApiImpl(
private val client: KtorfitClient,
) : ExampleApi {
public override suspend fun exampleGet(): People {
val requestData = RequestData(method="GET",
relativeUrl="/test",
returnTypeData=TypeData("com.example.model.People"))
return client.suspendRequest<People, People>(requestData)!!
}
}
public fun Ktorfit.createExampleApi(): ExampleApi = _ExampleApiImpl(KtorfitClient(this))
After that the source code of the Ktorfit.create() will be generated. The same function in the Ktorfit-lib is only there to enable code completion inside IntelliJ.
The source code of the generated function will look something likes this :
public inline fun <reified T> Ktorfit.create(): T = when(T::class){
com.example.ExampleApi::class ->{
this.createExampleApi() as T
}
else ->{
throw IllegalArgumentException("Could not find any Ktorfit annotations in class"+ T::class.simpleName )
}
}
For every interface with annotations that KSP found there will be a case in the when block. And an instance of the implementation will be returned. Otherwise the IllegalArgumentException will be thrown.
The problem
This works when the interfaces are in the inside the same module where the create-function is used.
This does not work when you use a interface that is inside another module.
Example:
Module A: Uses MyService from ModuleB -> create< MyService>()
Module B: contains the MyService.kt
KSP only knows about the files that are used inside the module, where it is currently used.
KSP in ModuleA will generate the create() function and doesn't know that ModuleB exists. So no code for MyService will be generated and it can't be used and at Runtime it will throw the IllegalArgumentException from the else case
KSP in Module B will generate the create() and the implementation class of MyService.
Why is Ktorfit implemented this way?
I wanted to provide a similar easy usage like Retrofit and i want that this library can be used in multiplatform projects. The big problem is, unlike Retrofit i can`t use reflection, because their is basically none for KotlinJS and Kotlin Native available.
Because of this, we need to know the possible interfaces at compile time to add them to Ktorfit.create() and can't just generate code on runtime.
Ideas
- Maybe there is a option in KSP, that i don`t know
- Maybe completely removing the generic create-function and only using the other extension functions like "createExampleApi()" could be a way. In the example above, when Module B adds "build/generated" as source folder. Then Module A should be able to find the createMyService() function. And it should work.