Kronos helps you manage your Android/iOS app's remote configs with ease.
Define your remote config contract using kotlin property delegation and keep you remote configs in order.
interface SomeAppFeatureConfig {
val syncIntervalMinutes: Long
}
class SomeAppFeatureKronosConfig : SomeAppFeatureConfig, KronosConfig {
override val sourceDefinition = typedSource<FirebaseConfigSource>()
override val syncIntervalMinutes by longConfig {
default = 60
minValue = 15
process { it.milliseconds.inWholeMinutes }
}
}
val someAppFeatureConfig: SomeAppFeatureConfig = SyncFeatureKronosConfig()
someAppFeatureConfig.syncIntervalMinutes
The SDK should be initialized once, preferably in the Application/AppDelegate class
Kronos.init {
logging {
enabled = true
logger = NapierLogger()
}
configSource { FirebaseConfigSource() }
}
Kronos can potentially support any config source, e.g Firebase Remote Config.
To add a config sources to Kronos, implement either the ConfigSource
or the MutableConfigSource
interface.
Config sources can be either added when initializing the SDK or at runtime by retrieving the config source repository and adding the source to it.
Kronos.configSourceRepository.addSource(FirebaseConfigSource())
Implement the KronosConfig
interface:
- Define sourceDefinition, i.e what source should be used to resolve the config values.
- Define your config properties using Kronos config extension functions.
class SomeAppFeatureKronosConfig : SomeAppFeatureConfig, KronosConfig {
override val sourceDefinition = typedSource<FirebaseConfigSource>()
}
If a config source should only be scoped to a specific feature, you can define a scoped source and provide an instance:
class SomeAppFeatureKronosConfig(source: ConfigSource) : SomeAppFeatureConfig, KronosConfig {
override val sourceDefinition = scopedSource(source)
}
Kronos uses Kotlin's property delegates to define the config contract.
A class implementing the KronosConfig
will get access to config properties extension functions.
The the following config property types are supported: Int, Long, Float, Double, String, Boolean, Set
For each there's a config delegate extension function, i.e intConfig
, longConfig
, floatConfig
, doubleConfig
, stringConfig
, booleanConfig
, stringSetConfig
.
Config properties are not nullable, if you need to use nullable configs, you can use the nullable delegates, e.g nullableIntConfig
.
Each config property must define a default value.
To provide a default value you can either assign the default
property or provide a default value lambda to be resolved lazily.
class SomeAppFeatureKronosConfig : SomeAppFeatureConfig, KronosConfig {
override val sourceDefinition = typedSource<FirebaseConfigSource>()
override val syncIntervalMinutes by longConfig {
key = "some_feature_sync_interval_minutes"
default = 60
}
}
If a config key is not specified, Kronos will use the property name as a key to resolve the config value from the source. Alternatively, you can specify a custom key by assigning the key property.
A config property can either be a val
or a var
.
If var
is used, make sure the property config source implements MutableConfigSource
.
A config property can override the sourceDefinition of it's containing class by assigning the sourceDefinition property inside its builder.
Kronos allows you to process the remotely config value before returning it.
In the following example the remove value is configured in milliseconds, but the app uses minutes as the time unit.
val syncIntervalMinutes by longConfig {
default = 60
process { it.milliseconds.inWholeMinutes }
}
Sometimes, as part of the config processing, we want to convert it to another type, Kronos supports this using adapted configs:
val syncIntervalMinutes by adaptedIntConfig<String> {
default = 60
adapt {
get { it.toString() }
}
}
Setting a default value to an adapted config can either be done by assigning the default
property or the primitiveDefault
property if you wish to provide a primitive default which will be adapted as well.
If the adaptation process is not light, and you want to cache the resolved config value after its first read, you can set the cached
property to true
.
Sometimes remote config values can be invalid, e.g negative values for time configs. Kronos allows you to validate the remotely configured value before returning it by defining constraints.
val syncIntervalMinutes by longConfig {
default = 60
constraint {
allowIf { it >= 0 }
fallbackTo = 15
}
}
If validation fails, Kronos will return the default value. Alternatively, you can provide a fallback value for each constraint to be returned in case of validation failure.
val syncIntervalMinutes by longConfig {
default = 60
constraint {
allowIf { it >= 0 }
fallbackTo = 15
}
}
Sometimes you don't need a special implementation for your constraint, in that case you can use the built-in suite of constraints Kronos provides, such as:
minValue
, maxValue
, allowList
and more!
If you have a custom constraint you want to reuse across configs, this can easily be achieved by creating an extension function/property on the com.hrofeh.kronos.config.Config
class.
Sometimes you'll find yourself using the same adaptation and validation logic across multiple config properties. In such cases, Kronos offers a easy way to define custom config properties that can be reused.
Define an extension function on the KronosConfig
interface with your config name and use the ConfigPropertyFactory
to create the config property delegate:
data class Label(val value: String)
fun KronosConfig.labelConfig(block: Config<String, Label>.() -> Unit) =
ConfigPropertyFactory.from(
configSourceResolver = ConfigSourceResolver.String,
validator = { it.isNotEmpty() },
getterAdapter = { Label(it) },
setterAdapter = { it.value },
block = block
)
Custom config properties support all Kronos capabilities, such as processing, adapting and constraints.
Kronos is available on MavenCentral.
dependencies {
implementation 'com.hrofeh.kronos:kronos:VERSION'
}
The kronos base module is meant to be lean and serve common use-cases. As your app's remote config gets more advanced, you can use Kronos' extension modules:
Kronos provides a Json extension module to create json config properties.
When initializing the SDK, you need to initialize the extension and provide a json serializer:
Kronos.init {
extension {
json {
serializer = KronosKotlinxSerializer()
}
}
}
Kronos doesn't have a built-in json serializer, but you can either:
- Use any json serializer that implements the
KronosJsonSerializer
interface. - Use the kotlinx-serialization json serializer by adding the the extension module.
Then you can use the jsonConfig
extension to define json config properties:
val syncDays by jsonConfig<List<Int>> {
default = emptyList()
}
Adding the json extension dependency:
dependencies {
implementation 'com.hrofeh.kronos:extension-json:VERSION'
}
To use the kotlinx-serialization json serializer, add the following dependency:
dependencies {
implementation 'com.hrofeh.kronos:extension-json-kotlinx:VERSION'
}
Copyright (c) 2024 Hanan Rofe Haim
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.