Skip to content

Repository files navigation

KtProvider

Maven Central Sonatype Nexus (Snapshots)

中文文档

Cross-Module Service Provisioning Framework with KMP Support

  • Multi-module Project with KMP Support
  • Suitable for use in Compose Multiplatform environment.

Setup

Across all modules:

Kotlin Multiplatform projects
// build.gradle.kts
plugins {
  id("com.google.devtools.ksp")
  id("io.github.985892345.KtProvider") version "x.y.z"
}

kotlin {
  sourceSets {
    commonMain.dependencies {
      // The provider-manager dependency can be optionally added. 
      // Additionally, you have the option to implement your own provider-manager.
      implementation(ktProvider.manager)
    }
  }
}

dependencies {
  // Please refer to the official documentation for KSP configurations: 
  // https://kotlinlang.org/docs/ksp-multiplatform.html
  add("kspCommonMainMetadata", ktProvider.ksp)
  add("kspAndroid", ktProvider.ksp)
  add("kspDesktop", ktProvider.ksp) // dependent on JVM source set configuration
  add("kspIosX64", ktProvider.ksp)
  add("kspIosArm64", ktProvider.ksp)
  add("kspIosSimulatorArm64", ktProvider.ksp)
  // ...
  
  // The provider-api dependency is already included with the Gradle plugin.
}
Only Jvm or Android projects
// build.gradle.kts
plugins {
  id("com.google.devtools.ksp")
  id("io.github.985892345.KtProvider") version "x.y.z"
}

dependencies {
  // The provider-manager dependency can be optionally added. 
  // Additionally, you have the option to implement your own provider-manager.
  implementation(ktProvider.manager)
  // ksp
  ksp(ktProvider.ksp)
}

Initialization

Kotlin/Jvm: It is recommended to perform initialization in the main function.

fun main() {
  // Invoking the generated XXXKtProviderInitializer (Module Name + KtProviderInitializer) via KSP
  XXXKtProviderInitializer.tryInitKtProvider()
}

Android: It is recommended to perform initialization in the Application#onCreate method.

class App : Application() {
  override fun onCreate() {
    super.onCreate()
    XXXKtProviderInitializer.tryInitKtProvider()
  }
}

iOS: I'm not proficient in iOS, so this may not be the most optimal approach.

Swift: App#init

@main
struct iOSApp: App {
    init() {
        XXXKtProviderInitializer.shared.tryInitKtProvider()
    }
}

Objective-C: application:didFinishLaunchingWithOptions:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [XXXKtProviderInitializer.shared tryInitKtProvider];
}

Usage

Add interface

interface ITestService {
  fun get(): String
}

Implement interface

@ImplProvider(clazz = ITestService::class, name = "test")
class TestServiceImpl : ITestService {
  override fun get(): String {
    return "123"
  }
}
Annotation
ImplProvider Obtain an instance, which becomes a singleton when the implementation class is an object.
KClassProvider Retrieve the KClass of the implementation class (can be used for Class<out Activity> in Android).

Use interface

val service = KtProvider.implOrNull(ITestService::class, "test")
println(service.get())

Testing

Add provider-testing to the test source set. The main source set still needs provider-manager as shown in Setup.

Kotlin Multiplatform projects
kotlin {
  sourceSets {
    commonTest.dependencies {
      implementation(kotlin("test"))
      implementation(ktProvider.testing)
    }
  }
}
JVM or Android projects
dependencies {
  testImplementation(ktProvider.testing)
}

Use the initializer generated for the module under test. withKtProviderTest creates an isolated registry, loads the module and the KtProvider project-module dependencies recorded in its generated initializer, redirects KtProvider lookups for the block, and restores the production resolver when the block returns or throws.

import com.g985892345.provider.testing.withKtProviderTest
import kotlin.test.Test
import kotlin.test.assertEquals

private object FakeTestService : ITestService {
  override fun get(): String = "fake"
}

class ServiceConsumerTest {

  @Test
  fun usesTheTestProvider() = withKtProviderTest(ModuleKtProviderInitializer) {
    overrideImpl<ITestService>(name = "test") {
      FakeTestService
    }

    assertEquals(
      "fake",
      KtProvider.impl(ITestService::class, "test").get(),
    )
  }
}

Implementation-class routes can be replaced in the same way:

withKtProviderTest(ModuleKtProviderInitializer) {
  overrideKClass<ITestService>(name = "test") {
    FakeTestService::class
  }
}

The generated ModuleKtProviderInitializer is produced by the main KSP task. Running the test task also runs the required main compilation and KSP generation. If the reference is unresolved immediately after writing the first test, run Gradle sync or the test task once.

Each withKtProviderTest call owns a fresh registry, so overrides do not mutate production registrations or leak into later sequential tests. The block is synchronous and resolver scopes must not overlap; do not run multiple withKtProviderTest blocks concurrently in the same test process.

Implementation principle

  1. The KtProvider's Gradle plugin automatically resolves the inter-module dependencies and passes this information to KSP to generate an implementation class for KtProviderInitializer.
  2. KSP scans annotations to generate an implementation class for KtProviderRouter.

Similar to the following code:

// Implementation class of KtProviderInitializer
object ModuleKtProviderInitializer : KtProviderInitializer() {
  
  override val router: KtProviderRouter = ModuleKtProviderRouter
  
  override val otherModuleKtProvider: List<KtProviderInitializer> = listOf(
    // Here, based on the module dependency relationship during Gradle compilation, 
    // the implementation class of KtProviderInitializer for the dependent modules is generated.
    Module1KtProviderInitializer,
    Module2KtProviderInitializer
  )
}
// Implementation class of KtProviderRouter
internal object ModuleKtProviderRouter : KtProviderRouter() {
  override fun initRouter(delegate: IKtProviderDelegate) {
    // By using KSP, find all annotations and then add the implementation classes.
    delegate.addImplProvider(ITestService::class, "abc") { TestServiceImpl }
    delegate.addImplProvider(ITestService2::class, "123") { TestServiceImpl2 }
  }
}

License

Copyright 2023 985892345

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

Cross-module service providing framework for Kotlin Multiplatform Project (KMP) support.(支持 KMP 的跨模块服务提供框架)

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages