Reactant Config System

Config Model

To start using the reactant config system, you need to firstly define the config model class, following is an example of a config model class.

The data keyword is not a must, but it can be useful on debugging while you can easily log the content out.

Remind that the config fields should be var instead of val, and all fields should have a default value.

data class HelloConfig(
var helloMessages: ArrayList<String> = arrayListOf("Msg1", "Msg2", "Msg3"),
var helloRate: Double = 0.7
// Another example that without "data" keyword, and nested object
class ComplicatedHelloConfig {
var defaultHello = HelloConfig()
var helloConfigs = HashMap<String, HelloConfig>()

Basically, the config supports the common data structures and nested objects, but it is depends on the config parser. By default, the parsers we provided doesn't support polymorphism on the fields. (i.e. You can't declare something like "Any" type fields, every field type should be a concrete class)

Config Injections

Reactant provided some injection candies which are easily to use for the simple use case, those injectable support json, yaml/yml and toml format.


To inject a config as a non-shared object with the specified config model class, we can use Config<T> and specify the model class as T. ConfigService will automatically read the file by the path provided in the @Inject(), and use a corresponding ParserService based on the filename extension.

The config file will be automatically created if it doesn't exist.

import dev.reactant.reactant.service.spec.config.Config
import dev.reactant.reactant.core.dependency.injection.Inject
class ExampleComponent(
private val helloConfig: Config<HelloConfig>
) : LifeCycleHook {
override fun onEnable(){
if ( Random.nextDouble() < helloConfig.content.helloRate ){;

save() / refresh() / remove()

You can use the functions of Config<T> to save, refresh (i.e. discard changes and reload content from disk) and remove the config. The content of config after being removed will still be accessible from Config<T>, but will be removed from the disk.

All operations are Completable (Similar to Observable, but without result), so that you can easily make it asynchronous.

override fun onEnable(){
// try to change helloRate to zero
config.content.helloRate = 0
// synchronous refresh, blocking at main thread, value should be roll backed
// example of asynchronous operation, make sure you know what you are doing!
override fun onSave(){

In most of the cases, the operations in onEnable() / onDisable() / onSave() should be synchronoused in order to ensure the operations was completed inside the hook.


Config<T> will generate a new Config instance for each injections, which will not share the content state between each component until you call the refresh() function.

Sometimes we may need to synchronize the state between each component, therefore we need SharedConfig<T>.

Basically it is similar to Config<T>:

private val helloConfig: SharedConfig<HelloConfig>


In some cases, you will need to manage a list of configs which having the same model. For example, multiple configs of 'CustomFoodItemConfig' inside folder plugins/MyFirstPlugin/foods, we can just inject it like the previous example.

private val foodConfigs: MultiConfig<FoodConfig>

With the MultiConfig object, you can use following functions to get/create configs. You may need to use io scheduler if you would like to do those operations after initialization.

// Get config "plugins/MyFirstPlugin/foods/apple.json"
.doOnError { /* not exist */ }
.subscribe { /* exist */ }
// Get or return default if not exist
foodConfigs.getOrDefault("banana.json"){ FoodConfig() }
.subscribe { /* do sth. */ }
// Get or create default if not exist
foodConfigs.getOrPut("cake.json"){ FoodConfig() }
.subscribe { /* do sth. */ }
// Get all configs as observable, by default recursively load
foodConfigs.getAll(true).subscribe { /* do sth. */ }
// Get all configs with file name as a map, by default recursively load
foodConfigs.getAllAsMap(true).subscribe { /* do sth. */ }

Since the result from the operations of MultiConfig<T> are also Config<T> objects, you can also access the operations of Config<T> as above described

// Load the apple.json, change the name, and save to disk.
.doOnSuccess { = "Orange" }
.flatMapCompletable { }
.subscribe {"Saved as orange!") }

Advanced usage of Config System

The injectables above are just the candies way for simple usage, if you have more complicated requirements, you should consider to use the ConfigService directly.

About ParserService

Before we start using ConfigService will use the ParserService to encode your config object to target format or decode it into a jvm object.

To use the common format like json,yml,toml, you are not required to write your own parser, Reactant come with the following default ParserService:

File formatInterfaceImplementation
json (suggested)JsonParserServiceGson
Customization of Gson

The default JsonParserService do not support customizing the Gson configuration, if you need to modify it (e.g. adding custom TypeAdapter), you need to create your own parser as following described.

Custom ParserService

To make your own parser, you need to implement the ParserService interface:

interface ParserService {
fun encode(obj: Any): Single<String>
fun <T : Any> decode(modelClass: KClass<T>, encoded: String): Single<T>

The injectables candies will only use the default parser provided by the reactant, to use your custom parser, you need directly call the ConfigService functions.

Use ConfigService directly