This is the opening post of a series that works through GTK4 and Libadwaita together, one practical interface decision at a time. The posts are grouped around the things you actually build — a window, a list, a settings page — rather than the library a widget happens to ship in. Each post stands alone; together they’re meant to be the field guide the reference documentation isn’t.


The problem with the reference

The GTK4 documentation is genuinely good. Every widget has a page. Every property, signal, and method is listed. The class hierarchies are accurate and the prose is precise. If you already know which widget you need, the reference will tell you everything about it.

That last sentence is the whole problem.

When you sit down to build a real interface, you almost never start from “I need a GtkColumnView.” You start from “I need to show a list of articles, some of them unread, sortable by date, and it has to feel right on a phone.” Nothing in the reference answers that. The reference is organised by what each widget is. The work is organised by what you’re trying to do — and the translation between those two is exactly the knowledge that doesn’t get written down.

So you do what everyone does. You search. You find a six-year-old Stack Overflow answer using GtkTreeView and GtkListStore, copy it, get it working, and only much later discover that the modern toolkit replaced that entire pattern with something completely different — list models and factories — that nobody told you about because the old way still compiles. You ship the deprecated path because it was the first thing that worked.

I’ve done this more times than I’d like to admit. This series is my attempt to write down the missing layer that the reference leaves to experience: which widget to reach for, when, and what’s going to bite you when you do.


What a “field guide” means here

A field guide isn’t an encyclopedia. A bird encyclopedia describes every feather; a field guide tells you how to tell a sparrow from a finch while it’s moving. That distinction drives every choice in this series.

Every post leads with a decision rather than a widget. The title of a typical entry is closer to “how do I show a list?” than “the GtkListView class” — the widget is the answer to a question I’ll pose first, in terms of a problem you actually have. And I’ll show the trade-offs rather than just the happy path, because GTK frequently hands you three widgets that all could do the job: GtkListBox, GtkListView, or GtkColumnView for a list; GtkBox or GtkGrid for layout; the new AdwDialog or the patterns it replaced. Knowing which to pick, and what each one costs you, is the actual skill.

Mostly, though, the gotcha is the point. Scrolling that silently doesn’t work because the policy is wrong; a factory that gets called far more often than you expected; rows that look identical but behave differently. These are the things that cost you an afternoon, and they’re exactly what the reference omits, because strictly speaking everything is working as documented. The flip side is that I won’t pad. GTK has a hundred-odd widgets and some of them, like GtkSeparator and GtkSpinner, don’t warrant a thousand words. Where a widget is trivial I’ll group it with its neighbours and tell you the one property you’ll actually reach for, rather than stretch a separator into a post.


The modern GTK mindset

Before any specific widget, there’s a shift in thinking that GTK4 quietly demands, and it trips up nearly everyone arriving from older GTK, other toolkits, or stale tutorials.

For years, the way you built a UI was imperative and row-oriented. You created widgets, packed them into containers, and when the data changed you went back into the widget tree and updated it by hand. I still have a lot of code that works exactly this way. It holds up fine until the data starts moving, at which point it fights you at every turn. And the specific machinery for displaying data that way — GtkTreeView, GtkListStore, cell renderers — is largely deprecated now. (Packing widgets into a GtkBox or GtkGrid is not going anywhere; it’s still how you build any non-list UI. It’s the row-oriented data-display path that’s on the way out.)

The modern way is model-driven. You keep your data in a list model, and you hand GTK a factory: a small recipe for turning one item into one row. GTK calls that factory as needed and recycles the widgets as the user scrolls. The recycling is the part that catches people out — your factory’s bind step runs again every time a row is reused for a different item, not just once when the widget is first created, so anything you set up has to be torn down again on unbind. You stop reaching into the tree when data changes; you update the model and let the UI follow.

If you’ve built a RecyclerView on Android or a UICollectionView on iOS, that recycle-and-bind rhythm will feel immediately familiar. If you’re coming from SwiftUI or Compose, adjust your expectations: GTK isn’t diffing a declarative view tree for you, so you still wire widgets up by hand inside the factory callbacks. And if you’re coming from GTK2 or 3, this is a real reorientation — one worth making deliberately rather than discovering by accident halfway through a project.

You’ll see this theme return constantly: describe the relationship between data and widget once, then let the toolkit run it. It shows up in list views, in property bindings, in GtkExpression, in the Libadwaita rows. Internalise it early and the rest of the toolkit stops surprising you.


How the series is organised

By concept, not by library.

The temptation is to do all of GTK first and then all of Libadwaita. I’m deliberately not doing that. Sorting widgets by the library they live in is an accident of how the code is packaged; it has almost nothing to do with the problem in front of you. Real interface work doesn’t divide along library lines. You don’t have “GTK problems” and “Libadwaita problems”; you have jobs: lay out a window, show a list, build a settings page, give the user feedback. So that’s how the posts are grouped. Each one takes a job and reaches for whichever toolkit has the right tool, and very often both are in play at once.

That last part matters more than it sounds. For a great many jobs, GTK gives you the raw mechanism and Libadwaita gives you the GNOME-shaped version on top of it: a GtkListBox row versus an AdwActionRow, a hand-assembled dialog versus an AdwAlertDialog, GtkHeaderBar versus AdwHeaderBar. Knowing the Libadwaita version exists, and knowing when to ignore it and reach for the GTK primitive instead, is the kind of judgement that only comes from use. The reference documents the two libraries as if they were strangers. They aren’t. You use them together, in the same widget tree, all day long. Libadwaita is also younger and far less blogged-about than GTK, so the example gap is widest exactly where the two meet, which is where this series spends most of its time.

Within each concept, posts are tiered honestly. Some carry a full treatment. Some get a tight, focused post. Some widgets are clustered with their siblings. I’ll always tell you which, so you know whether you’re getting a deep dive or a quick orientation. And where the GNOME Human Interface Guidelines have an opinion about the job at hand, I’ll point it out — following them is most of what makes an app feel native.


The example app: Gazette

Abstract examples are forgettable, so the series has a running one: Gazette, a small RSS reader. It’s the same app I use as the worked example throughout my Building GNOME Apps with Rust series, and it’s a good fit here because a feed reader naturally exercises most of the toolkit. A list of feeds. A list of articles within a feed. Article content. Search. Preferences. An adaptive layout that has to work on a laptop and a phone. Toasts when something syncs. Almost every widget I’ll cover has a real home in Gazette, so the examples are things you’d actually build, not contrived demos that fall apart the moment you change them.

You don’t need Gazette to follow along — every post stands on its own — but when I show code, it’ll be code that earns its place in a real app.


Why Rust

Every code sample in this series is Rust, using the gtk4-rs and libadwaita-rs bindings.

I made the case for Rust at length in the GNOME/Rust series, so I won’t relitigate it here. The short version: the bindings are mature and idiomatic, a lot of the most interesting new GNOME apps are being written in Rust and that’s the direction I’d bet on, and Rust’s type system catches a whole category of UI-state bugs before you run the program. It’s the default I’d recommend for new GNOME work in 2026.

If you write your GTK in C, Python, or Vala, you’re still in the right place. The decisions this series is about — which widget, when, and why — are language-neutral. The factory pattern is the factory pattern whether you express it in Rust closures or Python callbacks. You’ll just be mentally translating the snippets, and GTK’s consistent API naming across bindings makes that nearly mechanical.

What I won’t do is re-explain GObject subclassing or the project setup from scratch in every post — that ground is covered in the GNOME/Rust series, and I’ll link back to it rather than repeat it. If you want the full environment setup (Flatpak SDK, Builder, the toolchain), start there. For this series, a working gtk4-rs project and a copy of Workbench for quick experiments is all you really need.


Next up: the application shell

We start where every app starts: the window itself. GtkApplicationWindow versus AdwApplicationWindow, the AdwHeaderBar that sits on top of it, and AdwToolbarView for the bars above and below your content. You open on a plain GTK window and, within a few lines, reach for the Adwaita chrome that makes it look like it belongs on the desktop — the two libraries working together from line one.

From there we move outward, arranging things in space and then into the list machinery, which is where modern GTK really lives. If you’ve ever stared at a GtkListView wondering what a “factory” is and why your rows aren’t appearing, that’s the cluster of posts I’m most looking forward to writing.

One widget at a time. See you in the next one.