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.NavigationPage
s.
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!