Skip to main content

App building blocks

Views are the building blocks of Doodle apps. They encapsulate state, display content on the screen and respond to user input. Apps typically contain many View instances at runtime. And they often use a wide range of View types that provide specialized rendering and user interactions.

You create a new View by instantiating a custom type that extends the View base class, instantiating an inline object directly or through the view DSL.

package viewcreation import io.nacular.doodle.core.View import io.nacular.doodle.core.view //sampleStart class MyView: View() { // ... } val view1 = MyView() val view2 = object: View() { // ... } val view3 = view { // ... } //sampleEnd
tip

You will see how to get your Views onto the screen when we discuss the Display.

State and rendering

Below is an example of a View that holds some user data (name and age). It tracks this state and repaints itself whenever any of these properties is changed. This is done using renderProperty, which automatically triggers render on any value change.

Notice the render method in this class. It is responsible for all paint operations for a View, and it gets called whenever a View should be repainted. There are a number of reasons why render can be called, including: the first time a View is displayed, any time its size changes, or if there is an explicit call to rerender.

import io.nacular.doodle.core.View import io.nacular.doodle.core.renderProperty import io.nacular.doodle.drawing.Canvas import io.nacular.doodle.drawing.Color.Companion.Black import io.nacular.doodle.drawing.TextMetrics import io.nacular.doodle.drawing.text import io.nacular.doodle.geometry.Point //sampleStart class UserInfo(textMetrics: TextMetrics, name: String, age: Int): View() { var name by renderProperty(name) // causes repaint whenever changed var age by renderProperty(age ) private val nameHeight = textMetrics.height(name) override fun render(canvas: Canvas) { canvas.text("name: $name", color = Black) canvas.text("age : $age", at = Point(y = nameHeight), color = Black) } } //sampleEnd
tip

Learn more about rendering.

View hierarchies

Apps generally have one or more top-level View with other Views nested in them. Doodle allow you to do the same. You specify which Views will represent your top-level items and add them to the Display. These Views in turn will typically have nested Views that are either dynamically added, or part of the internal representation of their parent.

package applications import io.nacular.doodle.application.Application import io.nacular.doodle.core.Display import io.nacular.doodle.core.view //sampleStart class MyApp(display: Display): Application { init { display += view {} } // ... override fun shutdown() { /*...*/ } } //sampleEnd

All Views support nested children. However, they protect that list--and other traits related to being a container--by default, to improve encapsulation and API control.

Consider a split panel. It is reasonable to think about it as having a left and right child (ignoring orientation for now). However, an implementation of this concept might choose to have an additional child to represent the splitter. This choice is an internal detail of the implementation that would be leaked if the children list were public. Worse, a caller could remove the splitter or add more children than expected and break the behavior.

Doodle helps with these design challenges by letting you selectively expose a View's internals to callers.

import io.nacular.doodle.core.View import io.nacular.doodle.core.view //sampleStart class VSplitPanel: View() { var left: View? = null set(new) { if (new == field) { return } field?.let { children -= it } field = new field?.let { children += it } // notify of change } val right: View? = null // ... private val handle = view {} // private View for splitter init { children += handle // add handle to children } // ... } //sampleEnd

This design prevents direct access to the panel's children, which side-steps many issues. It also presents are more intuitive and reliable API. left and right are fairly self-documenting compared to children[n] and children[m]. Moreover, the panel is able to encapsulate the fact that it uses additional Views for presentation.