Using Input
Input is a Kotlin data class used to pass data and event listeners to the Formula instance. Let's say we need to
pass an item id to ItemDetailFormula
.
class ItemDetailFormula() : Formula<ItemDetailFormula.Input, ..., ...> {
// Input declaration
data class Input(val itemId: String)
// Use input to initialize state
override fun initialState(input: Input): State = ...
// Respond to Input changes.
override fun onInputChanged(oldInput: Input, input: Input, state: State): State {
// We can compare old and new inputs and create
// a new state before `Formula.evaluate` is called.
return state
}
// Using input within evaluate block
override fun evaluate(
input: Input,
state: ..,
context: ..
): Evaluation<...> {
val itemId = input.itemId
// We can use the input here to fetch the item from the repo.
}
}
To pass the input to ItemDetailFormula
val itemDetailFormula: ItemDetailFormula = ...
itemDetailFormula
.toObservable(ItemDetailFormula.Input(itemId = "1"))
.subscribe { renderModel ->
}
You could also pass an Observable<ItemDetailFormula.Input>
val itemDetailInput: Observable<ItemDetailFormula.Input> = ...
itemDetailFormula
.toObservable(itemDetailInput)
.subscribe { renderModel ->
}
Equality
Formula uses input equality to determine if it should re-evaluate. A parent can cause
a child formula to re-evaluate by changing the input it passes. This will also trigger
Formula.onInputChanged
on the child formula.
This is a desired behavior as we do want the child to react when the data that we pass changes.
data class ItemInput(
val itemId: String
)
Making Input
a data class and passing data as part of its properties makes it easy to reason
about its equality. In some cases though, we want to pass objects that don't have property based
equality such as listeners or observables.
Maintaining listener equality
In many cases we want to pass listeners to listen to formula events.
data class ItemListInput(
val onItemSelected: Listener<Item>
)
Formula provides an easy way to maintain listener equality. Within your parent formula,
you can use FormulaContext.onEvent
to instantiate listeners.
Don't: Don't instantiate functions within Formula.evaluate
.
override fun evaluate(...) {
val itemListInput = ItemListInput(
onItemSelected = {
analytics.track("item_selected")
}
)
}
Do: Use onEvent
override fun evaluate(...) {
val itemListInput = ItemListInput(
onItemSelected = context.onEvent { _ ->
transition { analytics.track("item_selected") }
}
)
}
Do: Use already constructed listeners
// Listener is constructed outside of the "evaluate" function block
val onItemSelectedListener = Listener<Item> {
}
override fun evaluate(...): ... {
val itemListInput = ItemListInput(
onItemSelected = onItemSelectedListener
)
}
Do: Delegate to parent input directly
override fun evaluate(input: Input, ...): ... {
val itemListInput = ItemListInput(
onItemSelected = input.onItemSelected
)
}
Passing observables
Observables have identity equality which make maintaining input equality a bit tricky.
data class MyInput(
val eventObservable: Observable<Event>
)
Don't: create a new observable within Formula.evaluate
override fun evaluate(...) {
val input = MyInput(
eventObservable = relay.map { Event() }
)
}
Do: create observable outside of Formula.evaluate
private val eventObservable = relay.map { Event() }
override fun evaluate(...) {
val input = MyInput(
eventObservable = eventObservable
)
}
Do: use State to instantiate observable once
data class State(
val eventObservable: Observable<Event>
)
override fun initialState(input: Input) = State(
eventObservable = input.eventObservable.map { Event() }
)
override fun evaluate(...) {
val input = MyInput(
eventObservable = state.eventObservable
)
}
Don't: pass data observables
data class Input(
val dataObservable: Observable<Data>
)
Do: pass data directly
data class Input(
val data: Data?
)