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 $ExampleProgressBar: Adw.Bin {
  ProgressBar progress_bar {}
}

This defines a template called ExampleProgressBar 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 ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    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 $ExampleProgressBar: Adw.Bin {
  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 (ExampleProgressBar‘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 ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    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 $ExampleNavigationView: Adw.Bin {
  Adw.NavigationView navigation_view {
    Adw.NavigationPage {
      child: $ExampleProgressBar progress_bar {};
    }

    Adw.NavigationPage {
      tag: "finished";

      child: Box {};
    }
  }
}

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

The first page has ExampleProgressBar 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 ExampleProgressBar

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

    __gtype_name__ = "ExampleNavigationView"

    navigation_view: Adw.NavigationView = Gtk.Template.Child()
    progress_bar: ExampleProgressBar = 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 ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    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)

ExampleProgressBar 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 $ExampleNavigationView: Adw.Bin {
  Adw.NavigationView navigation_view {
    Adw.NavigationPage {
      child: $ExampleProgressBar {
        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 ExampleProgressBar

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

    __gtype_name__ = "ExampleNavigationView"

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

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

In the code for ExampleNavigationView, 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 ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    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 ExampleProgressBar, 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!

Leave a Reply

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