Skip to main content

Controllers Pattern

The Controllers Pattern can be used to control the content from outside the component, typically from an ancestor, and for communication between the parent and children Components. A Controller is an object that can control the Primitive Component's content (a View or a Drawable) independently of the Component itself. In the example described below, the Controller is used to read and write the minute and hour values of the TimePicker view. The same instance of the Controller object is shared between the parent Component and both of its children, which allows children for controlling parent's behavior.

Controllers in Practice​

To demonstrate the use of controllers in practice, the following code implements a simple TimePicker Component, which can be used to show some arbitrary time: the code implements simple getters and setters of the TimePicker properties.

note

It's important to remember that the content is necessarily nullable because the content can get unmounted when it is out of the viewport. Any operation invoked on the controller should be memoized (saved) when the content is unbound so that it can be applied once the content is mounted. In the following code, notice how the minutes and hours are set to the vars in the setter methods and how, in bind, the values are set back on the content.

class TimePickerController(private var currentHour: Int, private var currentMinute: Int) {
var minute: Int
get() {
ThreadUtils.assertMainThread()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
content?.minute ?: currentMinute
} else {
content?.currentMinute ?: currentMinute
}
}
set(value) {
ThreadUtils.assertMainThread()
currentMinute = value
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
content?.minute = value
} else {
content?.currentMinute = value
}
}

var hour: Int
get() {
ThreadUtils.assertMainThread()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
content?.hour ?: currentHour
} else {
content?.currentHour ?: currentHour
}
}
set(value) {
ThreadUtils.assertMainThread()
currentHour = value
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
content?.hour = value
} else {
content?.currentHour = value
}
}

private val onTimeChangedListener =
TimePicker.OnTimeChangedListener { _, hour, minute ->
currentHour = hour
currentMinute = minute
}

private var content: TimePicker? = null

The Controller must be mounted and unmounted from the content manually from the bind and onUnbind methods inside of MountBehavior scope:

fun bind(content: TimePicker) {
this.content = content
hour = currentHour
minute = currentMinute
this.content?.setOnTimeChangedListener(onTimeChangedListener)
}

fun unbind() {
this.content?.setOnTimeChangedListener(null)
this.content = null
}

In the Primitive Component implementation, it's necessary to manually bind and unbind the controller with the content:

MountBehavior(ViewAllocator { context -> TimePicker(context) }) {
bind(controller) { content ->
controller?.bind(content)
onUnbind { controller?.unbind() }
}
}

The Primitive Component should pass the TimePickerPrimitiveComponent as a constructor parameter.

note

It's important to put the controller into the useCached hook so it's not recreated in each rerender. Otherwise the state won't be preserved across re-renders.

Any other methods and properties on the controller instance can be used easily in the code (see controller.hour = ...):

override fun ComponentScope.render(): Component {
return Column(style = Style.padding(16.dp)) {
val initialHour = 14
val initialMinute = 30
val controller =
useCached(initialHour, initialMinute) { TimePickerController(initialHour, initialMinute) }
child(TimePickerPrimitiveComponent(controller = controller))
child(
Button(
text = "Set random time ",
onClick = {
controller.hour = (0..24).random()
controller.minute = (0..60).random()
}))

Key points for Controllers​

  • The Primitive Component takes a controller as a constructor parameter.
  • If the controller is stateful, then it's necessary to hold the controller in state/cache otherwise state will be lost across re-renders.
  • Primitive should bind and unbind the controllers manually.
  • Controllers should maintain/update/watch state manually.