-
Notifications
You must be signed in to change notification settings - Fork 35
feat: Add KMP Product Flavors support #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import com.mobilebytelabs.kmpflavors.KmpFlavorExtension | ||
| import com.mobilebytelabs.kmpflavors.KmpFlavorPlugin | ||
| import org.convention.KmpFlavors | ||
| import org.convention.configureKmpFlavors | ||
| import org.gradle.api.Plugin | ||
| import org.gradle.api.Project | ||
| import org.gradle.kotlin.dsl.configure | ||
|
|
||
| /** | ||
| * Convention plugin that applies and configures KMP Product Flavors plugin. | ||
| * | ||
| * This plugin provides cross-platform flavor support for Kotlin Multiplatform modules, | ||
| * allowing consistent flavor configuration across Android, iOS, Desktop, and Web targets. | ||
| * | ||
| * Usage: | ||
| * ```kotlin | ||
| * plugins { | ||
| * id("org.convention.kmp.flavors") | ||
| * } | ||
| * ``` | ||
| * | ||
| * This will configure: | ||
| * - Demo/Prod flavors aligned with Android application flavors | ||
| * - BuildConfig generation with flavor-specific constants | ||
| * - Proper source set wiring for all platforms | ||
| */ | ||
| class KMPFlavorsConventionPlugin : Plugin<Project> { | ||
| override fun apply(target: Project) { | ||
| with(target) { | ||
| // Apply the KMP Product Flavors plugin by class | ||
| pluginManager.apply(KmpFlavorPlugin::class.java) | ||
|
|
||
| // Configure flavors using centralized configuration | ||
| extensions.configure<KmpFlavorExtension> { | ||
| configureKmpFlavors( | ||
| extension = this, | ||
| dimensions = KmpFlavors.defaultDimensions, | ||
| flavors = KmpFlavors.defaultFlavors, | ||
| generateBuildConfig = true, | ||
| buildConfigPackage = inferBuildConfigPackage(target), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Infers the BuildConfig package from the project's group or path. | ||
| */ | ||
| private fun inferBuildConfigPackage(project: Project): String { | ||
| // Try to use the project's group if available | ||
| val group = project.group.toString() | ||
| if (group.isNotEmpty() && group != "unspecified") { | ||
| return "$group.${project.name.replace("-", ".")}" | ||
| } | ||
|
|
||
| // Fall back to path-based package name | ||
| val pathParts = project.path | ||
| .removePrefix(":") | ||
| .split(":") | ||
| .filter { it.isNotEmpty() } | ||
|
|
||
| return if (pathParts.isNotEmpty()) { | ||
| "org.mifos.${pathParts.joinToString(".") { it.replace("-", ".") }}" | ||
| } else { | ||
| "org.mifos.${project.name.replace("-", ".")}" | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,195 @@ | ||||||||
| package org.convention | ||||||||
|
|
||||||||
| import com.mobilebytelabs.kmpflavors.FlavorConfig | ||||||||
| import com.mobilebytelabs.kmpflavors.KmpFlavorExtension | ||||||||
| import org.gradle.api.NamedDomainObjectContainer | ||||||||
|
|
||||||||
| /** | ||||||||
| * KMP Product Flavors configuration for kmp-project-template. | ||||||||
| * | ||||||||
| * This provides cross-platform flavor support that aligns with the existing | ||||||||
| * Android application flavors (demo/prod). | ||||||||
| * | ||||||||
| * ## Flavor Dimensions | ||||||||
| * - **contentType**: Demo vs Production content source | ||||||||
| * | ||||||||
| * ## Build Variants | ||||||||
| * - demo: Uses local/mock data for development and testing | ||||||||
| * - prod: Uses production backend services | ||||||||
| * | ||||||||
| * ## Usage in Modules | ||||||||
| * ```kotlin | ||||||||
| * plugins { | ||||||||
| * id("org.convention.kmp.flavors") | ||||||||
| * } | ||||||||
| * ``` | ||||||||
| */ | ||||||||
| object KmpFlavors { | ||||||||
|
|
||||||||
| /** | ||||||||
| * Flavor dimension definitions. | ||||||||
| * Currently matches Android's contentType dimension. | ||||||||
| */ | ||||||||
| enum class Dimension(val priority: Int) { | ||||||||
| CONTENT_TYPE(0) | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Available flavors aligned with Android application flavors. | ||||||||
| */ | ||||||||
| enum class Flavor( | ||||||||
| val dimension: Dimension, | ||||||||
| val isDefault: Boolean = false, | ||||||||
| val applicationIdSuffix: String? = null, | ||||||||
| val bundleIdSuffix: String? = null, | ||||||||
| ) { | ||||||||
| DEMO( | ||||||||
| dimension = Dimension.CONTENT_TYPE, | ||||||||
| isDefault = true, // Demo is default for development | ||||||||
| applicationIdSuffix = ".demo", | ||||||||
| bundleIdSuffix = ".demo", | ||||||||
| ), | ||||||||
| PROD( | ||||||||
| dimension = Dimension.CONTENT_TYPE, | ||||||||
| isDefault = false, | ||||||||
| ); | ||||||||
|
|
||||||||
| val flavorName: String = name.lowercase() | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Default dimension configurations for kmp-product-flavors. | ||||||||
| */ | ||||||||
| val defaultDimensions: List<DimensionConfig> | ||||||||
| get() = Dimension.values().map { dimension -> | ||||||||
| DimensionConfig( | ||||||||
| name = dimension.name.lowercase().replace("_", ""), | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, find and read the KmpFlavors.kt file
find . -name "KmpFlavors.kt" -type fRepository: openMF/kmp-project-template Length of output: 140 🏁 Script executed: #!/bin/bash
# Read the KmpFlavors.kt file to see lines around 66 and 78
cat -n ./build-logic/convention/src/main/kotlin/org/convention/KmpFlavors.ktRepository: openMF/kmp-project-template Length of output: 7080 🏁 Script executed: #!/bin/bash
# Search for contentType, contenttype, CONTENT_TYPE usage in the codebase
rg -i "contenttype|content_type" --type=kt -C 3Repository: openMF/kmp-project-template Length of output: 97 🏁 Script executed: #!/bin/bash
# Search for contentType, contenttype, CONTENT_TYPE usage in the codebase
rg -i "contenttype|content_type" -C 3Repository: openMF/kmp-project-template Length of output: 5960 🏁 Script executed: #!/bin/bash
# Let's look at the AppFlavor.kt file more closely to understand the dimension naming
cat -n ./build-logic/convention/src/main/kotlin/org/convention/AppFlavor.ktRepository: openMF/kmp-project-template Length of output: 2182 🏁 Script executed: #!/bin/bash
# Check if there's any test or usage that validates the dimension names match
rg "contentType\|contenttype" --type-list | head -20
find . -name "*.gradle.kts" -o -name "build.gradle" | xargs grep -l "contentType\|contenttype\|kmp.*flavor" 2>/dev/null | head -10Repository: openMF/kmp-project-template Length of output: 519 🏁 Script executed: #!/bin/bash
# Check the build.gradle.kts file for kmp-product-flavors configuration
cat -n ./build-logic/convention/build.gradle.kts | head -80Repository: openMF/kmp-project-template Length of output: 3032 🏁 Script executed: #!/bin/bash
# Search for how kmp-product-flavors is actually used and if there's any configuration or test
find . -type f \( -name "*.kt" -o -name "*.kts" \) | xargs grep -l "kmpProductFlavors\|KmpFlavorExtension\|configureKmpFlavors" 2>/dev/nullRepository: openMF/kmp-project-template Length of output: 253 🏁 Script executed: #!/bin/bash
# Read the KMPFlavorsConventionPlugin.kt to see how it's being used
cat -n ./build-logic/convention/src/main/kotlin/KMPFlavorsConventionPlugin.ktRepository: openMF/kmp-project-template Length of output: 2902 🏁 Script executed: #!/bin/bash
# Search for any test files that might validate dimension names
find . -type f -name "*Test*.kt" -o -name "*test*.kts" | xargs grep -l "contenttype\|contentType\|dimension" 2>/dev/null | head -5Repository: openMF/kmp-project-template Length of output: 53 🏁 Script executed: #!/bin/bash
# Search for any gradle configuration files that might show how dimension names are used
find . -type f -name "*.gradle.kts" | xargs grep -A5 -B5 "dimension\|flavorDimension" 2>/dev/null | head -50Repository: openMF/kmp-project-template Length of output: 53 Dimension name transform produces "contenttype" instead of "contentType" — this mismatches Android configuration. Android's The transform should convert Suggested fix for camelCase conversion- name = dimension.name.lowercase().replace("_", ""),
+ name = dimension.name.lowercase()
+ .replace(Regex("_([a-z])")) { it.groupValues[1].uppercase() },Apply the same to line 78. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| priority = dimension.priority, | ||||||||
| ) | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Default flavor configurations for kmp-product-flavors. | ||||||||
| */ | ||||||||
| val defaultFlavors: List<FlavorConfigData> | ||||||||
| get() = Flavor.values().map { flavor -> | ||||||||
| FlavorConfigData( | ||||||||
| name = flavor.flavorName, | ||||||||
| dimension = flavor.dimension.name.lowercase().replace("_", ""), | ||||||||
| isDefault = flavor.isDefault, | ||||||||
| applicationIdSuffix = flavor.applicationIdSuffix, | ||||||||
| bundleIdSuffix = flavor.bundleIdSuffix, | ||||||||
| ) | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Checks if a specific flavor is currently active. | ||||||||
| */ | ||||||||
| fun isFlavorActive(flavor: Flavor, activeVariant: String): Boolean = | ||||||||
| activeVariant.contains(flavor.flavorName, ignoreCase = true) | ||||||||
|
|
||||||||
| /** | ||||||||
| * Gets the base URL for the current flavor. | ||||||||
| */ | ||||||||
| fun getBaseUrl(flavor: Flavor): String = when (flavor) { | ||||||||
| Flavor.DEMO -> "https://demo-api.mifos.org" | ||||||||
| Flavor.PROD -> "https://api.mifos.org" | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Gets the analytics enabled flag for the current flavor. | ||||||||
| */ | ||||||||
| fun isAnalyticsEnabled(flavor: Flavor): Boolean = when (flavor) { | ||||||||
| Flavor.DEMO -> false | ||||||||
| Flavor.PROD -> true | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Data class for dimension configuration. | ||||||||
| */ | ||||||||
| data class DimensionConfig( | ||||||||
| val name: String, | ||||||||
| val priority: Int, | ||||||||
| ) | ||||||||
|
|
||||||||
| /** | ||||||||
| * Data class for flavor configuration. | ||||||||
| */ | ||||||||
| data class FlavorConfigData( | ||||||||
| val name: String, | ||||||||
| val dimension: String, | ||||||||
| val isDefault: Boolean = false, | ||||||||
| val applicationIdSuffix: String? = null, | ||||||||
| val bundleIdSuffix: String? = null, | ||||||||
| val buildConfigFields: Map<String, BuildConfigFieldData> = emptyMap(), | ||||||||
| ) | ||||||||
|
|
||||||||
| /** | ||||||||
| * Data class for BuildConfig field. | ||||||||
| */ | ||||||||
| data class BuildConfigFieldData( | ||||||||
| val type: String, | ||||||||
| val value: String, | ||||||||
| ) | ||||||||
|
|
||||||||
| /** | ||||||||
| * Extension function to configure KMP flavors using centralized configuration. | ||||||||
| */ | ||||||||
| fun configureKmpFlavors( | ||||||||
| extension: KmpFlavorExtension, | ||||||||
| dimensions: List<DimensionConfig>, | ||||||||
| flavors: List<FlavorConfigData>, | ||||||||
| generateBuildConfig: Boolean = true, | ||||||||
| buildConfigPackage: String? = null, | ||||||||
| ) { | ||||||||
| extension.apply { | ||||||||
| this.generateBuildConfig.set(generateBuildConfig) | ||||||||
| buildConfigPackage?.let { this.buildConfigPackage.set(it) } | ||||||||
|
|
||||||||
| // Configure dimensions | ||||||||
| flavorDimensions { | ||||||||
| dimensions.forEach { dim -> | ||||||||
| register(dim.name) { | ||||||||
| priority.set(dim.priority) | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // Configure flavors | ||||||||
| this.flavors { | ||||||||
| flavors.forEach { flavorData -> | ||||||||
| register(flavorData.name) { | ||||||||
| dimension.set(flavorData.dimension) | ||||||||
| isDefault.set(flavorData.isDefault) | ||||||||
| flavorData.applicationIdSuffix?.let { applicationIdSuffix.set(it) } | ||||||||
| flavorData.bundleIdSuffix?.let { bundleIdSuffix.set(it) } | ||||||||
|
|
||||||||
| // Add standard BuildConfig fields | ||||||||
| addStandardBuildConfigFields(this, flavorData) | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Adds standard BuildConfig fields to a flavor. | ||||||||
| */ | ||||||||
| private fun addStandardBuildConfigFields( | ||||||||
| flavorConfig: FlavorConfig, | ||||||||
| flavorData: FlavorConfigData, | ||||||||
| ) { | ||||||||
| val flavor = KmpFlavors.Flavor.values().find { it.flavorName == flavorData.name } | ||||||||
| if (flavor != null) { | ||||||||
| flavorConfig.apply { | ||||||||
| buildConfigField("String", "BASE_URL", "\"${KmpFlavors.getBaseUrl(flavor)}\"") | ||||||||
| buildConfigField("Boolean", "ANALYTICS_ENABLED", KmpFlavors.isAnalyticsEnabled(flavor).toString()) | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // Add any custom fields | ||||||||
| flavorData.buildConfigFields.forEach { (name, field) -> | ||||||||
| flavorConfig.buildConfigField(field.type, name, field.value) | ||||||||
| } | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,6 +54,7 @@ spotlessVersion = "7.0.2" | |
| turbine = "1.2.1" | ||
| twitter-detekt-compose = "0.0.26" | ||
| moduleGraph = "2.9.0" | ||
| kmpProductFlavors = "1.0.0" | ||
|
|
||
| # Kotlin KMP Dependencies | ||
| kotlin = "2.2.21" | ||
|
|
@@ -251,6 +252,7 @@ kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx- | |
| kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } | ||
|
|
||
| ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } | ||
| kmpProductFlavors-gradlePlugin = { group = "io.github.mobilebytelabs.kmpflavors", name = "flavor-plugin", version.ref = "kmpProductFlavors" } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if the artifact exists on Maven Central
echo "Checking Maven Central for io.github.mobilebytelabs.kmpflavors:flavor-plugin:1.0.0"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://repo1.maven.org/maven2/io/github/mobilebytelabs/kmpflavors/flavor-plugin/1.0.0/flavor-plugin-1.0.0.pom")
echo "HTTP Response Code: $HTTP_CODE"
if [ "$HTTP_CODE" == "200" ]; then
echo "✓ Artifact is available on Maven Central"
elif [ "$HTTP_CODE" == "404" ]; then
echo "✗ Artifact NOT found on Maven Central (404)"
else
echo "? Unexpected response code: $HTTP_CODE"
fiRepository: openMF/kmp-project-template Length of output: 221 The artifact Confirm the artifact group ID, name, and version are correct. If the artifact is published to an alternative repository, add that repository to the project's Gradle configuration. If this is a custom or private artifact, ensure the appropriate repository and credentials are configured. 🤖 Prompt for AI Agents |
||
|
|
||
| ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktorVersion" } | ||
| ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktorVersion" } | ||
|
|
@@ -367,6 +369,7 @@ aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "abo | |
| cmp-feature-convention = { id = "org.convention.cmp.feature", version = "unspecified" } | ||
| kmp-koin-convention = { id = "org.convention.kmp.koin", version = "unspecified" } | ||
| kmp-library-convention = { id = "org.convention.kmp.library", version = "unspecified" } | ||
| kmp-flavors-convention = { id = "org.convention.kmp.flavors", version = "unspecified" } | ||
| kmp-core-base-library-convention = { id = "org.convention.kmp.core.base.library", version = "unspecified" } | ||
|
|
||
| android-application-firebase = { id = "org.convention.android.application.firebase" } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Package name inference may produce invalid Kotlin/Java packages.
A few concerns with
inferBuildConfigPackage:Line 53:
project.name.replace("-", ".")doesn't sanitize characters that are invalid in Java/Kotlin package identifiers (e.g., names starting with a digit, or containing other special characters). A module named3d-rendererwould produce a segment starting with3d.Line 63: Same issue with
pathParts.joinToString(".")— path segments could begin with digits or contain other invalid chars.Lines 63, 65: The
"org.mifos."prefix is hardcoded. Since this is a project template intended for forking, downstream users will inheritorg.mifosBuildConfig packages unless they set an explicit group. Consider deriving this from a configurable property or at minimum documenting that users should setproject.group.Suggested defensive sanitization
🤖 Prompt for AI Agents