
Remote-config functionality: in-app defaults, cloud fetching with ETag-aware status, local caching/throttling, decoupled fetch-and-activate flow, JSON parsing and configurable fetch/release settings.
A Kotlin Multiplatform SDK for Nona Config, replicating core features of Firebase Remote Config: defaults, fetching, caching, and activation.
Add the dependency directly to your build.gradle.kts (available on Maven Central, no extra repository or credentials required):
dependencies {
implementation("io.github.rfaturriza:nona-config:1.0.2")
}https://github.com/rfaturriza/NonaConfigKMP
1.0.2.NonaConfig to your app target.If your project uses a Package.swift manifest, add the dependency to your Package definition:
// Package.swift
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/rfaturriza/NonaConfigKMP", from: "1.0.2")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "NonaConfig", package: "NonaConfigKMP")
]
)
]
)val nonaConfig = NonaConfig.instance
nonaConfig.initialize(
apiKey = "your-api-key",
environmentId = "production",
baseUrl = "https://your-config-server.com"
)nonaConfig.setDefaults(mapOf(
"welcome_message" to "Hello!",
"feature_enabled" to true
))// Option A: Callback-based non-suspend function (ideal for UI & non-coroutine callers)
nonaConfig.fetchAndActivate { success ->
if (success) {
val message = nonaConfig.getString("welcome_message")
}
}
// Option B: Coroutine suspend function
coroutineScope.launch {
val success = nonaConfig.fetchAndActivate()
}You can inspect the detailed fetch status (e.g. distinguishing 200 OK from 304 Not Modified), read the current ETag, or clear it to force a fresh fetch:
// Fetch with explicit status result
nonaConfig.fetchAndActivateWithStatus { status ->
when (status) {
NonaConfig.FetchStatus.SUCCESS_NEW_DATA -> println("HTTP 200 OK - Downloaded new payload")
NonaConfig.FetchStatus.SUCCESS_NOT_MODIFIED -> println("HTTP 304 - ETag matched, using cached config")
NonaConfig.FetchStatus.THROTTLED -> println("Fetch throttled by minimum fetch interval")
NonaConfig.FetchStatus.ERROR -> println("Fetch failed")
}
}
// Inspect active ETag
val currentETag: String? = nonaConfig.lastETag
// Clear ETag to force unconditional fetch on next request (omits If-None-Match)
nonaConfig.clearETag()val message = nonaConfig.getString("welcome_message")
val isEnabled = nonaConfig.getBoolean("feature_enabled")
// Advanced: Parsing JSON values directly
val theme = nonaConfig.getValue("theme_settings").asJson(ThemeConfig.serializer())val settings = NonaConfigSettings.Builder()
.setBaseUrl("https://your-custom-backend.com") // Optional: Custom server URL
.setMinimumFetchInterval(1.hours)
.setReleaseVersion("1.1.x") // Pin to a specific release line
.build()
nonaConfig.setConfigSettings(settings)Do not hardcode your API key. Use the Secrets Gradle Plugin to inject it from local.properties.
local.properties (Git ignored)
NONA_API_KEY=your_actual_keybuild.gradle.kts
plugins {
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}MainActivity.kt
NonaConfig.instance.initialize(
apiKey = BuildConfig.NONA_API_KEY,
environmentId = "production",
baseUrl = "https://your-config-server.com"
)Do not hardcode your API key. Use a Git-ignored Secrets.xcconfig file included in Config.xcconfig.
Secrets.xcconfig (Git ignored)
NONA_API_KEY=your_actual_keyConfig.xcconfig (Tracked in Git)
#include? "Secrets.xcconfig"
TEAM_ID=
PRODUCT_NAME=NonaConfigKMP
PRODUCT_BUNDLE_IDENTIFIER=com.nonaconfig.NonaConfigKMP$(TEAM_ID)Info.plist
<key>NONA_API_KEY</key>
<string>$(NONA_API_KEY)</string>Swift Initialization
import NonaConfig // The name of your framework
let client = NonaConfigClient.companion.instance
// Securely getting the key from Info.plist or Environment
let apiKey = (Bundle.main.object(forInfoDictionaryKey: "NONA_API_KEY") as? String)
.flatMap { $0.isEmpty ? nil : $0 }
?? ProcessInfo.processInfo.environment["NONA_API_KEY"]
?? ""
client.initialize(apiKey: apiKey, environmentId: "production", baseUrl: "https://your-config-server.com")Swift Usage
// Setting Defaults
client.setDefaults(defaults: ["welcome_message": "Hello Swift!"])
// Fetch and Activate (Async/Await)
Task {
do {
let success = try await client.fetchAndActivate()
if success {
let message = client.getString(key: "welcome_message")
print(message)
}
} catch {
print("Fetch failed: \(error)")
}
}The library is configured for Maven publication. To publish to a local repository for testing:
./gradlew publishToMavenLocalReleases automatically publish binary XCFramework artifacts and update Package.swift via KMMBridge during CI.
To test building the XCFramework locally:
./gradlew :sharedLogic:assembleXCFrameworkApache License 2.0
A Kotlin Multiplatform SDK for Nona Config, replicating core features of Firebase Remote Config: defaults, fetching, caching, and activation.
Add the dependency directly to your build.gradle.kts (available on Maven Central, no extra repository or credentials required):
dependencies {
implementation("io.github.rfaturriza:nona-config:1.0.2")
}https://github.com/rfaturriza/NonaConfigKMP
1.0.2.NonaConfig to your app target.If your project uses a Package.swift manifest, add the dependency to your Package definition:
// Package.swift
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/rfaturriza/NonaConfigKMP", from: "1.0.2")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "NonaConfig", package: "NonaConfigKMP")
]
)
]
)val nonaConfig = NonaConfig.instance
nonaConfig.initialize(
apiKey = "your-api-key",
environmentId = "production",
baseUrl = "https://your-config-server.com"
)nonaConfig.setDefaults(mapOf(
"welcome_message" to "Hello!",
"feature_enabled" to true
))// Option A: Callback-based non-suspend function (ideal for UI & non-coroutine callers)
nonaConfig.fetchAndActivate { success ->
if (success) {
val message = nonaConfig.getString("welcome_message")
}
}
// Option B: Coroutine suspend function
coroutineScope.launch {
val success = nonaConfig.fetchAndActivate()
}You can inspect the detailed fetch status (e.g. distinguishing 200 OK from 304 Not Modified), read the current ETag, or clear it to force a fresh fetch:
// Fetch with explicit status result
nonaConfig.fetchAndActivateWithStatus { status ->
when (status) {
NonaConfig.FetchStatus.SUCCESS_NEW_DATA -> println("HTTP 200 OK - Downloaded new payload")
NonaConfig.FetchStatus.SUCCESS_NOT_MODIFIED -> println("HTTP 304 - ETag matched, using cached config")
NonaConfig.FetchStatus.THROTTLED -> println("Fetch throttled by minimum fetch interval")
NonaConfig.FetchStatus.ERROR -> println("Fetch failed")
}
}
// Inspect active ETag
val currentETag: String? = nonaConfig.lastETag
// Clear ETag to force unconditional fetch on next request (omits If-None-Match)
nonaConfig.clearETag()val message = nonaConfig.getString("welcome_message")
val isEnabled = nonaConfig.getBoolean("feature_enabled")
// Advanced: Parsing JSON values directly
val theme = nonaConfig.getValue("theme_settings").asJson(ThemeConfig.serializer())val settings = NonaConfigSettings.Builder()
.setBaseUrl("https://your-custom-backend.com") // Optional: Custom server URL
.setMinimumFetchInterval(1.hours)
.setReleaseVersion("1.1.x") // Pin to a specific release line
.build()
nonaConfig.setConfigSettings(settings)Do not hardcode your API key. Use the Secrets Gradle Plugin to inject it from local.properties.
local.properties (Git ignored)
NONA_API_KEY=your_actual_keybuild.gradle.kts
plugins {
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}MainActivity.kt
NonaConfig.instance.initialize(
apiKey = BuildConfig.NONA_API_KEY,
environmentId = "production",
baseUrl = "https://your-config-server.com"
)Do not hardcode your API key. Use a Git-ignored Secrets.xcconfig file included in Config.xcconfig.
Secrets.xcconfig (Git ignored)
NONA_API_KEY=your_actual_keyConfig.xcconfig (Tracked in Git)
#include? "Secrets.xcconfig"
TEAM_ID=
PRODUCT_NAME=NonaConfigKMP
PRODUCT_BUNDLE_IDENTIFIER=com.nonaconfig.NonaConfigKMP$(TEAM_ID)Info.plist
<key>NONA_API_KEY</key>
<string>$(NONA_API_KEY)</string>Swift Initialization
import NonaConfig // The name of your framework
let client = NonaConfigClient.companion.instance
// Securely getting the key from Info.plist or Environment
let apiKey = (Bundle.main.object(forInfoDictionaryKey: "NONA_API_KEY") as? String)
.flatMap { $0.isEmpty ? nil : $0 }
?? ProcessInfo.processInfo.environment["NONA_API_KEY"]
?? ""
client.initialize(apiKey: apiKey, environmentId: "production", baseUrl: "https://your-config-server.com")Swift Usage
// Setting Defaults
client.setDefaults(defaults: ["welcome_message": "Hello Swift!"])
// Fetch and Activate (Async/Await)
Task {
do {
let success = try await client.fetchAndActivate()
if success {
let message = client.getString(key: "welcome_message")
print(message)
}
} catch {
print("Fetch failed: \(error)")
}
}The library is configured for Maven publication. To publish to a local repository for testing:
./gradlew publishToMavenLocalReleases automatically publish binary XCFramework artifacts and update Package.swift via KMMBridge during CI.
To test building the XCFramework locally:
./gradlew :sharedLogic:assembleXCFrameworkApache License 2.0