Data Driven UI With Closures

It’s highly recommended to read my previous blog post first to understand some of the topics discussed here.

UI can be hard to keep track of when changed imperatively, preferably it just follows the code’s state. Closures provide an intuitive way to do so by having data as input, and the desired value as output. They couple data with UI, but decouple the specific piece of UI that’s changed, making closures very modular. The example in this post uses Python and Blueprint.

Technicalities

First, it’s good to be familiar with the technical details behind closures. To quote from Blueprint’s documentation:

Expressions are only reevaluated when their inputs change. Because Blueprint doesn’t manage a closure’s application code, it can’t tell what changes might affect the result. Therefore, closures must be pure, or deterministic. They may only calculate the result based on their immediate inputs, not properties of their inputs or outside variables.

To elaborate, expressions know when their inputs have changed due to the inputs being GObject properties, which emit the “notify” signal when modified.

Another thing to note is where casting is necessary. To again quote Blueprint’s documentation:

Blueprint doesn’t know the closure’s return type, so closure expressions must be cast to the correct return type using a cast expression.

Just like Blueprint doesn’t know about the return type, it also doesn’t know the type of ambiguous properties. To provide an example:

Button simple_button {
  label: _("Click");
}

Button complex_button {
  child: Adw.ButtonContent {
    label: _("Click");
  };
}

Getting the label of simple_button in a lookup does not require a cast, since label is a known property of Gtk.Button with a known type:

simple_button.label

While getting the label of complex_button does require a cast, since child is of type Gtk.Widget, which does not have the label property:

complex_button.child as <Adw.ButtonContent>.label

Example

To set the stage, there’s a window with a Gtk.Stack which has two Gtk.StackPages, one for the content and one for the loading view:

from gi.repository import Adw, Gtk


@Gtk.Template.from_resource("/org/example/App/window.ui")
class Window(Adw.ApplicationWindow):
    """The main window."""

    __gtype_name__ = "Window"
using Gtk 4.0;
using Adw 1;

template $Window: Adw.ApplicationWindow {
  title: _("Demo");

  content: Adw.ToolbarView {
    [top]
    Adw.HeaderBar {}

    content: Stack {
      StackPage {
        name: "content";

        child: Label {
          label: _("Meow World!");
        };
      }

      StackPage {
        name: "loading";

        child: Adw.Spinner {};
      }
    };
  };
}

Switching Views Conventionally

One way to manage the views would be to rely on signals to communicate when another view should be shown:

from typing import Any

from gi.repository import Adw, GObject, Gtk


@Gtk.Template.from_resource("/org/example/App/window.ui")
class Window(Adw.ApplicationWindow):
    """The main window."""

    __gtype_name__ = "Window"

    stack: Gtk.Stack = Gtk.Template.Child()

    loading_finished = GObject.Signal()

    @Gtk.Template.Callback()
    def _show_content(self, *_args: Any) -> None:
        self.stack.set_visible_child_name("content")

A reference to the stack has been added, as well as a signal to communicate when loading has finished, and a callback to run when that signal is emitted.

using Gtk 4.0;
using Adw 1;

template $Window: Adw.ApplicationWindow {
  title: _("Demo");
  loading-finished => $_show_content();

  content: Adw.ToolbarView {
    [top]
    Adw.HeaderBar {}

    content: Stack stack {
      StackPage {
        name: "content";

        child: Label {
          label: _("Meow World!");
        };
      }

      StackPage {
        name: "loading";

        child: Adw.Spinner {};
      }
    };
  };
}

A signal handler has been added, as well as a name for the Gtk.Stack.

Only a couple of changes had to be made to switch the view when loading has finished, but all of them are sub-optimal:

  1. A reference in the code to the stack would be nice to avoid
  2. Imperatively changing the view makes following state harder
  3. This approach doesn’t scale well when the data can be reloaded, it would require another signal to be added

Switching Views With a Closure

To use a closure, the class needs data as input and a method to return the desired value:

from typing import Any

from gi.repository import Adw, GObject, Gtk


@Gtk.Template.from_resource("/org/example/App/window.ui")
class Window(Adw.ApplicationWindow):
    """The main window."""

    __gtype_name__ = "Window"

    loading = GObject.Property(type=bool, default=True)

    @Gtk.Template.Callback()
    def _get_visible_child_name(self, _obj: Any, loading: bool) -> str:
        return "loading" if loading else "content"

The signal has been replaced with the loading property, and the template callback has been replaced by a method that returns a view name depending on the value of that property. _obj here is the template class, which is unused.

using Gtk 4.0;
using Adw 1;

template $Window: Adw.ApplicationWindow {
  title: _("Demo");

  content: Adw.ToolbarView {
    [top]
    Adw.HeaderBar {}

    content: Stack {
      visible-child-name: bind $_get_visible_child_name(template.loading) as <string>;

      StackPage {
        name: "content";

        child: Label {
          label: _("Meow World!");
        };
      }

      StackPage {
        name: "loading";

        child: Adw.Spinner {};
      }
    };
  };
}

In Blueprint, the signal handler has been removed, as well as the unnecessary name for the Gtk.Stack. The visible-child-name property is now bound to a closure, which takes in the loading property referenced with template.loading.

This fixed the issues mentioned before:

  1. No reference in code is required
  2. State is bound to a single property
  3. If the data reloads, the view will also adapt

Closing Thoughts

Views are just one UI element that can be managed with closures, but there’s plenty of other elements that should adapt to data, think of icons, tooltips, visibility, etc. Whenever you’re writing a widget with moving parts and data, think about how the two can be linked, your future self will thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *