-
Notifications
You must be signed in to change notification settings - Fork 37
feat: Integrate KMP Product Flavors v1.0.1 #133
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
base: dev
Are you sure you want to change the base?
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,73 @@ | ||
| 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. | ||
| * Ensures the package name is valid Kotlin (no hyphens). | ||
| */ | ||
| 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") { | ||
| // Replace hyphens with dots to ensure valid Kotlin package name | ||
| val sanitizedGroup = group.replace("-", ".") | ||
| val sanitizedName = project.name.replace("-", ".") | ||
| return "$sanitizedGroup.$sanitizedName" | ||
| } | ||
|
|
||
| // Fall back to path-based package name | ||
| val pathParts = project.path | ||
| .removePrefix(":") | ||
| .split(":") | ||
| .filter { it.isNotEmpty() } | ||
|
|
||
| return if (pathParts.isNotEmpty()) { | ||
| // Replace hyphens with underscores to ensure valid Kotlin identifiers | ||
| "org.mifos.${pathParts.joinToString(".") { it.replace("-", "_") }}" | ||
| } else { | ||
| "org.mifos.${project.name.replace("-", "_")}" | ||
| } | ||
| } | ||
|
Comment on lines
+50
to
+72
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. Inconsistent hyphen sanitization strategy between the two package-inference code paths. The group-based path (Line 55-56) replaces Using underscores uniformly (valid Kotlin package identifiers) in both paths avoids this split-vs-collapse mismatch: ♻️ Proposed fix if (group.isNotEmpty() && group != "unspecified") {
- val sanitizedGroup = group.replace("-", ".")
- val sanitizedName = project.name.replace("-", ".")
+ val sanitizedGroup = group.replace("-", "_")
+ val sanitizedName = project.name.replace("-", "_")
return "$sanitizedGroup.$sanitizedName"
}🤖 Prompt for AI Agents |
||
| } | ||
| 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 -> | ||
|
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. 🛠️ Refactor suggestion | 🟠 Major Replace deprecated
♻️ Proposed fix- get() = Dimension.values().map { dimension ->
+ get() = Dimension.entries.map { dimension ->- get() = Flavor.values().map { flavor ->
+ get() = Flavor.entries.map { flavor ->- val flavor = KmpFlavors.Flavor.values().find { it.flavorName == flavorData.name }
+ val flavor = KmpFlavors.Flavor.entries.find { it.flavorName == flavorData.name }Also applies to: 75-75, 183-183 🤖 Prompt for AI Agents |
||
| DimensionConfig( | ||
| name = dimension.name.lowercase().replace("_", ""), | ||
| 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) | ||
| } | ||
| } | ||
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.
Run Dependency Guard to baseline the new
implementationdependency.kmpProductFlavors.gradlePluginis added asimplementation(correctly, sinceKmpFlavorPluginis applied by class reference), but this adds the artifact to the runtime dependency set of all convention plugin consumers. The Dependency Guard baseline must be updated to reflect this change.As per coding guidelines: Use Dependency Guard to validate dependencies in Kotlin Multiplatform projects.
🤖 Prompt for AI Agents