Providers¶
What are Providers?¶
If you've written a Gradle plugin before, the concept of Nova's Provider<T> and MutableProvider<T> will feel quite familiar to you, as they are similar to Gradle's Provider<T> and Property<T>.
A Provider<T> is a thread-safe container for a single, lazily-initialized value. You can create a very simple Provider<T> like this:
and then resolve its value via Provider.get():
Providers can be used to model lazy reactive data transformations:
val parent: MutableProvider<Int> = mutableProvider(1)
val child: Provider<Int> = parent.map { it * 2 }
println(child.get()) // 2
parent.set(2)
println(child.get()) // 4
You can also delegate a property to a provider. This can be useful for config-reloadable properties.
Fundamental Provider Functions¶
For Provider, the three fundamental transformation functions are:
Provider.map { ... }: TransformProvider<A>toProvider<B>Provider.flatMap { ... }: TransformAtoProvider<B>, but instead ofProvider<Provider<B>>, give me a flatProvider<B>combinedProvider(...): CombineProvider<A>,Provider<B>, ... to a single resultProvider<R>
With MutableProvider, you can also:
MutableProvider.map({ ... }, { ... }): Bi-directionally transform betweenMutableProvider<A>andMutableProvider<B>MutableProvider.decompose({ ... }, { ... }): Bi-directionally combine and take apart a value into multiple providers
There are many other transformation functions available, but they are mostly just syntax sugar that calls one of the these fundamental functions. Refer to the commons-provider KDocs for a full reference and additional details.
Weak Reference Semantics¶
Using the following example, we've constructed a provider graph like this:
val parent: MutableProvider<Int> =
mutableProvider { 0 }
val child: Provider<Int> =
parent.map { it * 2 }
flowchart TB
parent["parent : MutableProvider<Int>"]
child["child : Provider<Int>"]
child -->|strong<br>reference| parent
parent -. weak<br>reference .-> child
By default, parents reference their children weakly. This design allows you to have global top-level providers (for example for values of configs) and use them for shorter-lived objects without introducing memory leaks:
val PARENT: MutableProvider<Int> = mutableProvider { 0 }
fun foo() {
val child: Provider<Int> = PARENT.map { it * 2 } // (1)!
}
- If
PARENTwere to referencechildstronglychildwould be kept in memory indefinitely, creating a memory leak. Instead,childis referenced only weakly so it can be garbage-collected afterfoo().
You can also enforce strong child references by using the transformation functions prefixed with strong:
val parent: MutableProvider<Int> =
mutableProvider { 0 }
val child: Provider<Int> =
parent.strongMap { it * 2 }
flowchart TB
parent["parent : MutableProvider<Int>"]
child["child : Provider<Int>"]
child -->|strong<br>reference| parent
parent -->|strong<br>reference| child
Avoid Provider.subscribe and Provider.observe, if possible!
Due to the aforementioned weak reference semantics, it is recommended to avoid using Provider.subscribe or Provider.observe, if possible. If you do need to use it, you'll need to make sure that you either:
- store the provider that you subscribed to in a field (preferred, as it is simpler to reason about) or
- that the entire provider chain used transformations with strong reference semantics like
Provider.strongMap { }
Otherwise, your subscription may suddenly stop working as the Provider you subscribed to was garbage-collected.
Usage of Providers in Nova¶
Providers serve several use-cases in Nova:
- They are used as lazy values with convenient transformation functions. For example, when you register a custom item type, you'll get a
RegistryEntry<ItemType>, which is also aProvider<ItemType>. This is because the actualItemTypehas not yet been created, but you may still want to wire it somewhere (for example set it as the item type of a customBlockType). Providers help with this as you'll need to think less about initialization order or fight with circular dependencies. (If you are familiar with Minecraft's internals, think:Holder.Reference<*>) - They facilitate reloading mechanisms such as:
- config reloading: For example, you can have the data components of your custom item type depend on a value in a config. The config system gives you a provider, which you can transform to a data component and return in your item behavior.
- registry reloading: Registries can be reloaded to change their elements or tags. Both
RegistryEntry<*>andRegistryEntrySet<*>are providers and support reactive transformation functions. For example, you can transform aRegistryEntry<GuiTexture>to aProvider<Component>and wire it to the title of an InvUI Window. Then, when the gui texture registry is reloaded, the title updated appropriately.
- They manage state in InvUI's reactive API. Since it's all using the same API, you can easily make your InvUI GUIs support things like config- and registry reloading.
- They're used to lazily transform tile entity data after deserialization and before serialization.