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.
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 content?.minute ?: currentMinute
}
set(value) {
ThreadUtils.assertMainThread()
currentMinute = value
content?.minute = value
}
var hour: Int
get() {
ThreadUtils.assertMainThread()
return content?.hour ?: currentHour
}
set(value) {
ThreadUtils.assertMainThread()
currentHour = value
content?.hour = 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.
It's important to put the controller into the useCached hook so it's not recreated in each re-render. 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.