Skip to main content

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 View suits anything you'll reuse, or anything hefty enough to deserve its own named class — like UserInfo below.
  • An anonymous object: View() lands in between: a one-off View, 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
What's 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 View is displayed,
  • any time its size changes,
  • a renderProperty you're using changes (like name or age above), or
  • you explicitly ask with rerender.
tip

This page only covers enough rendering to explain render's role. The full Canvas section covers everything you can draw with Canvas — shapes, text, images, and more.

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:

  1. That handle becomes something callers can see and mess with, even though it's really an implementation detail.
  2. 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 View holds state, draws itself, and can respond to input. Create one with view { } for quick cases, or subclass for anything reusable.
  • renderProperty is a delegated property that calls render automatically whenever it changes — you never call render yourself.
  • Views nest inside each other. The ones added straight to the Display are top-level; everything else is nested inside some parent View.
  • A View's children list exists, but is usually kept private — expose a small, purpose-built API (like left/right) instead of the raw list.

Next: time to see everything you can actually put inside that render method — on to Canvas.