I use kvision, but only for the remoting layer. My...
# kvision
r
I use kvision, but only for the remoting layer. My frontend is currently Compose/Web. However, I do use the KVision project structure with
commonMain
,
frontendMain
, and
backendMain
. This is fine for now, but the next iteration of this will likely have desktop, android and iOS targets as well. I'll want a source set or library that has common code across all the frontends (but not backends, as some of that common code has no JVM platform target e.g. GitLive firebase-auth). Any advice/experience with structuring this kind of project? (More in comments)
My preferred layout would probably be 3 separate projects with a shared RPC library in a mono-repo with a gradle composite build: 1) KVision RPC project: contains
commonMain
,
frontendMain
,
backendMain
with all API interfaces but no backend implementations yet. Expose this as a multi-platform library. 2) Backend project depends on the RPC library, implements all the RPC interfaces, and contains the server-side Kvision init. 3) Frontend project which depends on the RPC library, has a commonMain across all the frontends that contains the logic to call the RPCs, and all of the frontend code. Is this type of project setup possible with KVision?
r
Hard to say. I haven't got time to play with new mpp layout changes introduced in 1.6.20 (https://kotlinlang.org/docs/whatsnew1620.html#hierarchical-structure-support-for-multiplatform-projects).
As I don't develop multiplatform projects (only fullstack webapps) you will have to try this yourself :-)
r
Thanks, I'll play around with it!
b
HMPP unfortunately is only available for native targets. My suggestion would be three modules: 1. shared 2. backend 3. client Then client module has a sourceSet per client target (web, desktop, android, ios, etc...
or even better yet, client has submodules for each target and itself acts only as a shared-client module with common client code
I usually find that mixing platform sourceSets in a single module only works well for library modules. App modules are best left isolated and depending on mpp library modules.
r
Yep, that's the basic structure I was hoping for.
One problem I'm running into related to kvision -- I define my RPC interfaces in the
shared
module. But I don't want to define my implementations there -- those should go in the
backend
module. Unfortunately, at build time I get:
Copy code
> Task :shared:compileKotlinBackend FAILED
e: /home/raman/source/foo/shared/build/generated-src/common/com/foo/rpcs/FooRpcServiceManager.kt: (9, 14): Expected class 'FooRpcService' has no actual declaration in module <shared> for JVM
Is there a Gradle incantation I can use to tell the compiler not to expect the actuals for the JVM platform in that module? Or do I have to create implementations in shared which delegate to the real implementations in
backend
?
b
There's an annotation to ignore missing actuals. Can't remember the name, but kotlin stdlib uses it all over the place
r
@OptionalExpectation
but isn't it only for annotations?
r
My Google-fu is failing me
Yeah if that's it, it does report
This annotation is not applicable to target 'class'
.
@Target(ANNOTATION_CLASS)
b
Well shit then
r
Basically its like a library that declares an
expect
but leaves the
actual
implementation to a consumer.
b
Oh, that's definitely not possible then.
Use interfaces for that
r
Ok, I'll have to create stubs in my shared project and inject the real implementation from the backend
I think something like this should work:
Copy code
interface RealIFooRpcService: IFooRpcService
actual class FooRpcService @Inject constructor(delegate: RealIFooRpcService) : IFooRpcService by delegate
b
Bingo! @Suppress("ACTUAL_WITHOUT_EXPECT")
There must be something like this for the other end of the stick
r
Expect_without_actual
does not seem to exist, still can't find anything similar.
I went ahead with the stub implementation for now. It was a bit trickier because of KVision's Guice injection of the ktor
ApplicationCall
. I change to delegate via a factory passing in call as a parameter, which works great, though its a bit boiler-platey:
Copy code
typealias RealIFooRpcServiceFactory = (ApplicationCall) -> RealIFooRpcService

interface RealIFooRpcService: IFooRpcService {
  val call: ApplicationCall
}

actual class FooRpcService @Inject constructor(factory: RealIFooRpcServiceFactory, call: ApplicationCall)
  : IFooRpcService by factory(call)
With that change (and dealing with a bunch of refactoring because smart-casts are not possible across module boundaries, argh -- https://youtrack.jetbrains.com/issue/KT-50534), the new project structure works great.