Tutorial / 15 minutes

Build a small screen that owns its own updates.

Create a project counter mounted in a Unity UIDocument. You will mount the tree, keep mutable data in State<T>, and dispose the UI deterministically.

Start with the UI Toolkit root you already own.

Create a GameObject, add UIDocument, and assign Panel Settings as you would for any UI Toolkit screen. LumaFlow does not replace this integration point; it mounts below the document’s root element.

Keep this boundary explicit.

Your MonoBehaviour owns the UIDocument and the returned MountHandle. The framework owns only its child host and the tree under that host.

Mount once when the host becomes active.

Create a component on the same GameObject. The first version can render a static screen; the lifecycle is already the production lifecycle.

ProjectCounterScreen.cs
using LumaFlow;
using UnityEngine;
using UnityEngine.UIElements;

using Framework = LumaFlow.LumaFlow;

[RequireComponent(typeof(UIDocument))]
public sealed class ProjectCounterScreen : MonoBehaviour {
  private MountHandle? _mount;

  private void OnEnable() {
    var root = GetComponent<UIDocument>().rootVisualElement;
    _mount = Framework.Mount(BuildScreen(), root);
  }

  private static Widget BuildScreen() =>
    new Text("Projects");
}

Make the changing value explicit.

A widget is an immutable description, so the counter lives outside the description. ReactiveBuilder<T> is the branch that reads the value and therefore the branch that updates.

ProjectCounterScreen.cs
private readonly State<int> _count = new(0);

private Widget BuildScreen() => new Column(
  gap: 12f,
  children: new Widget[] {
    new Text("Project overview"),
    new ReactiveBuilder<int>(
      _count, value => new Text($"{value} active projects")),
    new Button("Add project", () => _count.Value++)
  });

Do not instantiate long-lived controllers or state inside a builder. Builders can run again; the component is the owner that survives those updates.

Dispose when the document stops being your active host.

Disposal removes LumaFlow’s host and releases subscriptions. It does not clear native siblings that your application added to the document root.

ProjectCounterScreen.cs
private void OnDisable() {
  _mount?.Dispose();
  _mount = null;
}

Take the same model into a real application.

Responsive composition

Use LayoutBuilder to switch between compact and wide layouts from real constraints.

Application flow

Introduce a retained Navigator when the screen becomes a route.

Continue with the guides