Skip to content

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?
)