Views
On the Displays page, you added a couple of empty Views straight to the Display:
package display
import io.nacular.doodle.application.Application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.view
class SampleAppRecap(display: Display): Application {
init {
//sampleStart
val header = view {}
val sidebar = view {}
display += header
display += sidebar
//sampleEnd
}
override fun shutdown() { /*...*/ }
}
Time to find out what that view call was actually conjuring up — what a View really is, and how Views work together to build up everything your app shows.
A View is the basic building block of a Doodle app. It holds state, draws itself on screen, and can respond to user input: clicks, keystrokes, the works. A typical app ends up with many Views, some built into Doodle and some you write yourself, nested inside one another like Russian dolls.
Creating a View
Doodle gives you three ways to conjure one up:
package viewcreation
import io.nacular.doodle.core.View
import io.nacular.doodle.core.view
class MyView: View() {
// ...
}
//sampleStart
val view1 = view { /*...*/ } // DSL
val view2 = object: View() { /*...*/ } // Anonymous object
val view3 = MyView() // Subclassing
//sampleEnd
No need to memorize all three today — here's when each one earns its keep:
view { }(the DSL) is the quick option, and the one you already met on Displays. Reach for it with small, one-off Views that won't need to be reused.- Subclassing
Viewsuits anything you'll reuse, or anything hefty enough to deserve its own named class — likeUserInfobelow. - An anonymous
object: View()lands in between: a one-offView, but with more going on than the DSL comfortably holds.
Not sure which to pick? Default to view { }, and graduate to a subclass the moment a View earns a name's worth of responsibility.
Views react to their own state
Let's build a View that actually holds something — two pieces of state, name and age — and watch it redraw itself the instant either one changes:
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
by renderProperty(...)?This is Kotlin's property delegation — by hands control of a property to a helper object instead of storing it directly. renderProperty is that helper: it stores the value like a normal property would, but also calls render automatically any time the value changes. That's the whole trick behind UserInfo repainting itself — you never call render yourself when name or age changes, it just happens.
render(canvas: Canvas) is where all of a View's drawing happens, and — true to form — Doodle calls it for you rather than the other way around. The usual triggers are:
- the first time the
Viewis displayed, - any time its
sizechanges, - a
renderPropertyyou're using changes (likenameorageabove), or - you explicitly ask with
rerender.
Building a hierarchy
Most apps end up with one or more top-level Views — the ones added straight to the Display, like header and sidebar earlier — and each of those typically shelters more Views nested inside it:
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
Every View supports nested children, the same way Display does. But unlike Display, a View usually keeps that list to itself — and it's worth understanding why.
Why children are usually hidden
Picture a split panel — a View with a left side and a right side. Under the hood, a real implementation probably needs a third child too: the draggable handle between them. Make children a public, editable list, and two problems creep in:
- That handle becomes something callers can see and mess with, even though it's really an implementation detail.
- A caller could remove the handle, or add extra children, and quietly break how the panel works.
So instead of exposing children directly, each View picks a small, purpose-built API that hides those details:
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
Now callers only ever see left and right — two clearly named properties instead of a raw, indexable children list. The panel is free to add its handle, shuffle things internally, or rework its implementation entirely, without any of that leaking out through the public API.
Keep this pattern in your back pocket for any View that manages others internally: hold children close, and expose only the specific pieces callers actually need.
Recap
- A
Viewholds state, draws itself, and can respond to input. Create one withview { }for quick cases, or subclass for anything reusable. renderPropertyis a delegated property that callsrenderautomatically whenever it changes — you never callrenderyourself.- Views nest inside each other. The ones added straight to the
Displayare top-level; everything else is nested inside some parentView. - A
View'schildrenlist exists, but is usually kept private — expose a small, purpose-built API (likeleft/right) instead of the raw list.
Next: time to see everything you can actually put inside that render method — on to Canvas.