Skip to content

Build1 publisher3 min readPublished

Each bind on a recycled GtkListView row leaves another click handler behind

GTK builds one widget tree per row slot and hands that slot to a new item as you scroll. Anything bind connects without a matching unbind accumulates for the life of the app, with no crash and no warning.

The Engineer · Build desk

Illustration accompanying Each bind on a recycled GtkListView row leaves another click handler behind

What happened

  • GTK4's SignalListItemFactory exposes four connection points, setup, bind, unbind and teardown, and the earlier post in the dev.to field guide got a list on screen using only two of them.
  • Because add_css_class accumulates instead of replacing, nothing crashes or warns when a slot recycled from a done task onto a not-done one keeps rendering dim.
  • The author's fix is to write bind and unbind as a symmetrical pair, with unbind removing the CSS class unconditionally just before the slot is handed to a different item.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Keeping bind purely declarative is safe for CSS classes and forecloses the useful cases: a button handler, an async task tied to the item, or a GtkExpression watching a property all need the unbind half written.
  • cost The cost is deferred to production, where a slot rebound 200 times runs its callback 200 times on one click and the symptom reaches you as a user report, not a stack trace.
  • decision What bind touches decides how many of the four signals you connect.

setup and bind do not run the same number of times. setup builds the row's widget tree once per recycled row slot, not once per item [2]. bind attaches a specific item's data and runs on every assignment of an item to a slot, reuse included [3]. Scroll a 10,000-item list end to end and bind runs at least 10,000 times, while setup runs once for each slot GTK allocated [16]. That reuse is what lets the widget scroll ten thousand rows without building ten thousand rows [6].

The asymmetry in call counts is where the trap sits. `set_label` replaces the previous item's title on each call. `add_css_class` replaces nothing; it accumulates [7]. Recycle a slot from a completed task onto an unfinished one and `dim-label` is still on the label. Nothing crashes, nothing warns, and the row renders dim [8].

The instinct is to patch bind. The declarative version in the guide, `set_css_classes` with a conditional, covers this case, and for a single class the author calls it arguably fine [9]. It stops working the moment bind does something that is not purely declarative: a handler connected to a button inside the row, a spawned async task tied to the item, a `GtkExpression` watching a property [10]. Connect a handler in bind without disconnecting the old one and the second connection does not overwrite the first. The row fires its click callback twice, then three times, then four, silently, for as long as the app runs [11]. Absent a disconnect, the number of handlers on a slot equals the number of binds that slot has seen, so a slot rebound 200 times during a long scroll runs its callback 200 times on one click [15].

"It's the most common factory bug I've seen reported, and every instance I've traced back had the same root cause: something was set up in bind with no matching teardown in unbind," the author of the dev.to field guide wrote [12].

For the bug to reach you, the slot has to be reused. A list short enough that every item holds its own slot for the life of the window binds each slot once, and the missing unbind never shows [17]. So you hear about it from a user whose list is longer than the one you tested on.

The fix the post lands on is symmetry. bind and unbind are written as a pair, with the unbind handler removing the class unconditionally right before the slot is handed to a different item [13][4]. unbind does not need to know what bind decided. The fourth signal, teardown, fires when the widget tree is being destroyed for good, and the author reaches for it least of the four [5].

What to watch

  • The same guide says it covers a second way to build a factory that most tutorials skip; whether that route removes the need for hand-written unbind code.
  • A GTK-side diagnostic that warned when a handler is connected during bind would move this from developer discipline to tooling.
  • Whether the bind/unbind pairing extends cleanly to cancelling a spawned async task tied to the item, not just removing a CSS class.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories