UI-First Search With List Models

You can find the repository with the code here.

When managing large amounts of data, manual widget creation finds its limits. Not only because managing both data and UI separately is tedious, but also because performance will be a real concern.

Luckily, there’s two solutions for this in GTK:

1. Gtk.ListView using a factory: more performant since it reuses widgets when the list gets long
2. Gtk.ListBox‘s bind_model(): less performant, but can use boxed list styling

This blog post provides an example of a Gtk.ListView containing my pets, which is sorted, can be searched, and is primarily made in Blueprint.

The app starts with a plain window:

from gi.repository import Adw, Gtk


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

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

template $Window: Adw.ApplicationWindow {
  title: _("Pets");
  default-width: 450;
  default-height: 450;

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

Data Object

The Gtk.ListView needs a data object to work with, which in this example is a pet with a name and species.

This requires a GObject.Object called Pet with those properties, and a GObject.GEnum called Species:

from gi.repository import Adw, GObject, Gtk


class Species(GObject.GEnum):
    """The species of an animal."""

    NONE = 0
    CAT = 1
    DOG = 2

[…]

class Pet(GObject.Object):
    """Data for a pet."""

    __gtype_name__ = "Pet"

    name = GObject.Property(type=str)
    species = GObject.Property(type=Species, default=Species.NONE)

List View

Now that there’s a data object to work with, the app needs a Gtk.ListView with a factory and model.

To start with, there’s a Gtk.ListView wrapped in a Gtk.ScrolledWindow to make it scrollable, using the .navigation-sidebar style class for padding:

content: Adw.ToolbarView {
  […]

  content: ScrolledWindow {
    child: ListView {
      styles [
        "navigation-sidebar",
      ]
    };
  };
};

Factory

The factory builds a Gtk.ListItem for each object in the model, and utilizes bindings to show the data in the Gtk.ListItem:

content: ListView {
  […]

  factory: BuilderListItemFactory {
    template ListItem {
      child: Label {
        halign: start;
        label: bind template.item as <$Pet>.name;
      };
    }
  };
};

Model

Models can be modified through nesting. The data itself can be in any Gio.ListModel, in this case a Gio.ListStore works well.

The Gtk.ListView expects a Gtk.SelectionModel because that’s how it manages its selection, so the Gio.ListStore is wrapped in a Gtk.NoSelection:

using Gtk 4.0;
using Adw 1;
using Gio 2.0;

[…]

content: ListView {
  […]

  model: NoSelection {
    model: Gio.ListStore {
      item-type: typeof<$Pet>;

      $Pet {
        name: "Herman";
        species: cat;
      }

      $Pet {
        name: "Saartje";
        species: dog;
      }

      $Pet {
        name: "Sofie";
        species: dog;
      }

      $Pet {
        name: "Rex";
        species: dog;
      }

      $Pet {
        name: "Lady";
        species: dog;
      }

      $Pet {
        name: "Lieke";
        species: dog;
      }

      $Pet {
        name: "Grumpy";
        species: cat;
      }
    };
  };
};

Sorting

To easily parse the list, the pets should be sorted by both name and species.

To implement this, the Gio.ListStore has to be wrapped in a Gtk.SortListModel which has a Gtk.MultiSorter with two sorters, a Gtk.NumericSorter and a Gtk.StringSorter.

Both of these need an expression: the property that needs to be compared.

The Gtk.NumericSorter expects an integer, not a Species, so the app needs a helper method to convert it:

class Window(Adw.ApplicationWindow):
    […]

    @Gtk.Template.Callback()
    def _species_to_int(self, _obj: Any, species: Species) -> int:
        return int(species)
model: NoSelection {
  model: SortListModel {
    sorter: MultiSorter {
      NumericSorter {
        expression: expr $_species_to_int(item as <$Pet>.species) as <int>;
      }

      StringSorter {
        expression: expr item as <$Pet>.name;
      }
    };

    model: Gio.ListStore { […] };
  };
};

To learn more about closures, such as the one used in the Gtk.NumericSorter, consider reading my previous blog post.

Search

To look up pets even faster, the user should be able to search for them by both their name and species.

Filtering

First, the Gtk.ListView‘s model needs the logic to filter the list by name or species.

This can be done with a Gtk.FilterListModel which has a Gtk.AnyFilter with two Gtk.StringFilters.

One of the Gtk.StringFilters expects a string, not a Species, so the app needs another helper method to convert it:

class Window(Adw.ApplicationWindow):
    […]

    @Gtk.Template.Callback()
    def _species_to_string(self, _obj: Any, species: Species) -> str:
        return species.value_nick
model: NoSelection {
  model: FilterListModel {
    filter: AnyFilter {
      StringFilter {
        expression: expr item as <$Pet>.name;
      }

      StringFilter {
        expression: expr $_species_to_string(item as <$Pet>.species) as <string>;
      }
    };

    model: SortListModel { […] };
  };
};

Entry

To actually search with the filters, the app needs a Gtk.SearchBar with a Gtk.SearchEntry.

The Gtk.SearchEntry‘s text property needs to be bound to the Gtk.StringFilters’ search properties to filter the list on demand.

To be able to start searching by typing from anywhere in the window, the Gtk.SearchEntry‘s key-capture-widget has to be set to the window, in this case the template itself:

content: Adw.ToolbarView {
  […]

  [top]
  SearchBar {
    key-capture-widget: template;

    child: SearchEntry search_entry {
      hexpand: true;
      placeholder-text: _("Search pets");
    };
  }

  content: ScrolledWindow {
    child: ListView {
      […]

      model: NoSelection {
        model: FilterListModel {
          filter: AnyFilter {
            StringFilter {
              search: bind search_entry.text;
              […]
            }

            StringFilter {
              search: bind search_entry.text;
              […]
            }
          };

          model: SortListModel { […] };
        };
      };
    };
  };
};

Toggle Button

The Gtk.SearchBar should also be toggleable with a Gtk.ToggleButton.

To do so, the Gtk.SearchEntry‘s search-mode-enabled property should be bidirectionally bound to the Gtk.ToggleButton‘s active property:

content: Adw.ToolbarView {
  [top]
  Adw.HeaderBar {
    [start]
    ToggleButton search_button {
      icon-name: "edit-find-symbolic";
      tooltip-text: _("Search");
    }
  }

  [top]
  SearchBar {
    search-mode-enabled: bind search_button.active bidirectional;
    […]
  }

  […]
};

The search_button should also be toggleable with a shortcut, which can be added with a Gtk.ShortcutController:

[start]
ToggleButton search_button {
  […]

  ShortcutController {
    scope: managed;

    Shortcut {
      trigger: "<Control>f";
      action: "activate";
    }
  }
}

Empty State

Last but not least, the view should fall back to an Adw.StatusPage if there are no search results.

This can be done with a closure for the visible-child-name property in an Adw.ViewStack or Gtk.Stack. I generally prefer an Adw.ViewStack due to its animation curve.

The closure takes the amount of items in the Gtk.NoSelection as input, and returns the correct Adw.ViewStackPage name:

class Window(Adw.ApplicationWindow):
    […]

    @Gtk.Template.Callback()
    def _get_visible_child_name(self, _obj: Any, items: int) -> str:
        return "content" if items else "empty"
content: Adw.ToolbarView {
  […]

  content: Adw.ViewStack {
    visible-child-name: bind $_get_visible_child_name(selection_model.n-items) as <string>;
    enable-transitions: true;

    Adw.ViewStackPage {
      name: "content";

      child: ScrolledWindow {
        child: ListView {
          […]

          model: NoSelection selection_model { […] };
        };
      };
    }

    Adw.ViewStackPage {
      name: "empty";

      child: Adw.StatusPage {
        icon-name: "edit-find-symbolic";
        title: _("No Results Found");
        description: _("Try a different search");
      };
    }
  };
};

End Result

from typing import Any

from gi.repository import Adw, GObject, Gtk


class Species(GObject.GEnum):
    """The species of an animal."""

    NONE = 0
    CAT = 1
    DOG = 2


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

    __gtype_name__ = "Window"

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

    @Gtk.Template.Callback()
    def _species_to_string(self, _obj: Any, species: Species) -> str:
        return species.value_nick

    @Gtk.Template.Callback()
    def _species_to_int(self, _obj: Any, species: Species) -> int:
        return int(species)


class Pet(GObject.Object):
    """Data about a pet."""

    __gtype_name__ = "Pet"

    name = GObject.Property(type=str)
    species = GObject.Property(type=Species, default=Species.NONE)
using Gtk 4.0;
using Adw 1;
using Gio 2.0;

template $Window: Adw.ApplicationWindow {
  title: _("Pets");
  default-width: 450;
  default-height: 450;

  content: Adw.ToolbarView {
    [top]
    Adw.HeaderBar {
      [start]
      ToggleButton search_button {
        icon-name: "edit-find-symbolic";
        tooltip-text: _("Search");

        ShortcutController {
          scope: managed;

          Shortcut {
            trigger: "<Control>f";
            action: "activate";
          }
        }
      }
    }

    [top]
    SearchBar {
      key-capture-widget: template;
      search-mode-enabled: bind search_button.active bidirectional;

      child: SearchEntry search_entry {
        hexpand: true;
        placeholder-text: _("Search pets");
      };
    }

    content: Adw.ViewStack {
      visible-child-name: bind $_get_visible_child_name(selection_model.n-items) as <string>;
      enable-transitions: true;

      Adw.ViewStackPage {
        name: "content";

        child: ScrolledWindow {
          child: ListView {
            styles [
              "navigation-sidebar",
            ]

            factory: BuilderListItemFactory {
              template ListItem {
                child: Label {
                  halign: start;
                  label: bind template.item as <$Pet>.name;
                };
              }
            };

            model: NoSelection selection_model {
              model: FilterListModel {
                filter: AnyFilter {
                  StringFilter {
                    expression: expr item as <$Pet>.name;
                    search: bind search_entry.text;
                  }

                  StringFilter {
                    expression: expr $_species_to_string(item as <$Pet>.species) as <string>;
                    search: bind search_entry.text;
                  }
                };

                model: SortListModel {
                  sorter: MultiSorter {
                    NumericSorter {
                      expression: expr $_species_to_int(item as <$Pet>.species) as <int>;
                    }

                    StringSorter {
                      expression: expr item as <$Pet>.name;
                    }
                  };

                  model: Gio.ListStore {
                    item-type: typeof<$Pet>;

                    $Pet {
                      name: "Herman";
                      species: cat;
                    }

                    $Pet {
                      name: "Saartje";
                      species: dog;
                    }

                    $Pet {
                      name: "Sofie";
                      species: dog;
                    }

                    $Pet {
                      name: "Rex";
                      species: dog;
                    }

                    $Pet {
                      name: "Lady";
                      species: dog;
                    }

                    $Pet {
                      name: "Lieke";
                      species: dog;
                    }

                    $Pet {
                      name: "Grumpy";
                      species: cat;
                    }
                  };
                };
              };
            };
          };
        };
      }

      Adw.ViewStackPage {
        name: "empty";

        child: Adw.StatusPage {
          icon-name: "edit-find-symbolic";
          title: _("No Results Found");
          description: _("Try a different search");
        };
      }
    };
  };
}

List models are pretty complicated, but I hope that this example provides a good idea of what’s possible from Blueprint, and is a good stepping stone to learn more.

Thanks for reading!

PS: a shout out to Markus for guessing what I’d write about next ;)

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!

Cleaner Code With GObject

I see a lot of users approaching GNOME app development with prior language-specific experience, be it Python, Rust, or something else. But there’s another way to approach it: GObject-oriented and UI first.

This introduces more declarative code, which is generally considered cleaner and easier to parse. Since this approach is inherent to GTK, it can also be applied in every language binding. The examples in this post stick to Python and Blueprint.

Properties

While normal class properties for data work fine, using GObject properties allows developers to do more in UI through expressions.

Handling Properties Conventionally

Let’s look at a simple example: there’s a progress bar that needs to be updated. The conventional way of doing this would look something like the following:

using Gtk 4.0;
using Adw 1;

template $ProgressBar: Adw.Bin {
  child: ProgressBar progress_bar {};
}

This defines a template called ProgressBar which extends Adw.Bin and contains a Gtk.ProgressBar called progress_bar.

The reason why it extends Adw.Bin instead of Gtk.ProgressBar directly is because Gtk.ProgressBar is a final class, and final classes can’t be extended.

from gi.repository import Adw, GLib, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ProgressBar(Adw.Bin):

    __gtype_name__ = "ProgressBar"

    progress_bar: Gtk.ProgressBar = Gtk.Template.Child()

    progress = 0.0

    def __init__() -> None:
        super().__init__()

        self.load()

    def load(self) -> None:
        self.progress += 0.1
        self.progress_bar.set_fraction(self.progress)

        if int(self.progress) == 1:
            return

        GLib.timeout_add(200, self.load)

This code references the earlier defined progress_bar and defines a float called progress. When initialized, it runs the load method which fakes a loading operation by recursively incrementing progress and setting the fraction of progress_bar. It returns once progress is 1.

This code is messy, as it splits up the operation into managing data and updating the UI to reflect it. It also requires a reference to progress_bar to set the fraction property using its setter method.

Handling Properties With GObject

Now, let’s look at an example of this utilizing a GObject property:

using Gtk 4.0;
using Adw 1;

template $ProgressBar: Adw.Bin {
  child: ProgressBar {
    fraction: bind template.progress;
  };
}

Here, the progress_bar name was removed since it isn’t needed anymore. fraction is bound to the template’s (ProgressBar‘s) progress property, meaning its value is synced.

from gi.repository import Adw, GLib, GObject, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ProgressBar(Adw.Bin):

    __gtype_name__ = "ProgressBar"

    progress = GObject.Property(type=float)

    def __init__() -> None:
        super().__init__()

        self.load()

    def load(self) -> None:
        self.progress += 0.1

        if int(self.progress) == 1:
            return

        GLib.timeout_add(200, self.load)

The reference to progress_bar was removed in the code too, and progress was turned into a GObject property instead. fraction doesn’t have to be manually updated anymore either.

So now, managing the data and updating the UI merged into a single property through a binding, and part of the logic was put into a declarative UI file.

In a small example like this, it doesn’t matter too much which approach is used. But in a larger app, using GObject properties scales a lot better than having widget setters all over the place.

Communication

Properties are extremely useful on a class level, but once an app grows, there’s going to be state and data communication across classes. This is where GObject signals come in handy.

Handling Communication Conventionally

Let’s expand the previous example a bit. When the loading operation is finished, a new page has to appear. This can be done with a callback, a method that is designed to be called by another method, like so:

using Gtk 4.0;
using Adw 1;

template $NavigationView: Adw.Bin {
  child: Adw.NavigationView navigation_view {
    Adw.NavigationPage {
      child: $ProgressBar progress_bar {};
    }

    Adw.NavigationPage {
      tag: "finished";

      child: Box {};
    }
  };
}

There’s now a template for NavigationView, which extends an Adw.Bin for the same reason as earlier, which holds an Adw.NavigationView with two Adw.NavigationPages.

The first page has ProgressBar as its child, the other one holds a placeholder and has the tag “finished”. This tag allows for pushing the page without referencing the Adw.NavigationPage in the code.

from gi.repository import Adw, Gtk

from example.progress_bar import ProgressBar

@Gtk.Template(resource_path="/org/example/App/navigation-view.ui")
class NavigationView(Adw.Bin):

    __gtype_name__ = "NavigationView"

    navigation_view: Adw.NavigationView = Gtk.Template.Child()
    progress_bar: ProgressBar = Gtk.Template.Child()

    def __init__(self) -> None:
        super().__init__()

        def on_load_finished() -> None:
            self.navigation_view.push_by_tag("finished")

        self.progress_bar.load(on_load_finished)

The code references both navigation_view and progress_bar. When initialized, it runs the load method of progress_bar with a callback as an argument.

This callback pushes the Adw.NavigationPage with the tag “finished” onto the screen.

from typing import Callable

from gi.repository import Adw, GLib, GObject, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ProgressBar(Adw.Bin):

    __gtype_name__ = "ProgressBar"

    progress = GObject.Property(type=float)

    def load(self, callback: Callable) -> None:
        self.progress += 0.1

        if int(self.creation_progress) == 1:
            callback()
            return

        GLib.timeout_add(200, self.load, callback)

ProgressBar doesn’t run load itself anymore when initialized. The method also got an extra argument, which is the callback we passed in earlier. This callback gets run when the loading has finished.

This is pretty ugly, because the parent class has to run the operation now.

Another way to approach this is using a Gio.Action. However, this makes illustrating the point a bit more difficult, which is why a callback is used instead.

Handling Communication With GObject

With a GObject signal the logic can be reversed, so that the child class can communicate when it’s finished to the parent class:

using Gtk 4.0;
using Adw 1;

template $NavigationView: Adw.Bin {
  child: Adw.NavigationView navigation_view {
    Adw.NavigationPage {
      child: $ProgressBar {
        load-finished => $_on_load_finished();
      };
    }

    Adw.NavigationPage {
      tag: "finished";

      child: Box {};
    }
  };
}

Here, we removed the name of progress_bar once again since we won’t need to access it anymore. It also has a signal called load-finished, which runs a callback called _on_load_finished.

from gi.repository import Adw, Gtk

from example.progress_bar import ProgressBar

@Gtk.Template(resource_path="/org/example/App/navigation-view.ui")
class NavigationView(Adw.Bin):

    __gtype_name__ = "NavigationView"

    navigation_view: Adw.NavigationView = Gtk.Template.Child()

    @Gtk.Template.Callback()
    def _on_load_finished(self, _obj: ProgressBar) -> None:
        self.navigation_view.push_by_tag("finished")

In the code for NavigationView, the reference to progress_bar was removed, and a template callback was added, which gets the unused object argument. It runs the same navigation action as before.

from gi.repository import Adw, GLib, GObject, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ProgressBar(Adw.Bin):

    __gtype_name__ = "ProgressBar"

    progress = GObject.Property(type=float)
    load_finished = GObject.Signal()

    def __init__(self) -> None:
        super().__init__()

        self.load()

    def load(self) -> None:
        self.progress += 0.1

        if int(self.creation_progress) == 1:
            self.emit("load-finished")
            return

        GLib.timeout_add(200, self.load)

In the code for ProgressBar, a signal was added which is emitted when the loading is finished. The responsibility of starting the load operation can be moved back to this class too. The underscore and dash are interchangeable in the signal name in PyGObject.

So now, the child class communicates to the parent class that the operation is complete, and part of the logic is moved to a declarative UI file. This means that different parent classes can run different operations, while not having to worry about the child class at all.

Next Steps

Refine is a great example of an app experimenting with this development approach, so give that a look!

I would also recommend looking into closures, since it catches some cases where an operation needs to be performed on a property before using it in a binding.

Learning about passing data from one class to the other through a shared object with a signal would also be extremely useful, it comes in handy in a lot of scenarios.

And finally, experiment a lot, that’s the best way to learn after all.

Thanks to TheEvilSkeleton for refining the article, and Zoey for proofreading it.

Happy hacking!