LVGL Observers: Match Subscription Cleanup to Widget and Non-Widget Targets
Choose an observer registration API according to the target’s lifetime, then give every subscription exactly one cleanup path. Widget-associated observers can be removed automatically when that widget is deleted. A generic target pointer has no such lifecycle hook, even when the pointer happens to contain a widget address.
Applicability: LVGL v9.3.0 with LV_USE_OBSERVER enabled, one integer subject, one widget-associated callback and one non-widget callback under a serialized UI owner. Subject groups, external asynchronous callbacks and newer event-action helpers are outside this fixture. The callback-count sequences are proposed tests, not executed results.

1. Inventory subject, observer and target separately
The observer guide distinguishes a subject holding a value from dynamically allocated observers attached to it. The target is a third entity. Removing an observer ends a notification route; it does not generally destroy the target or the application model containing the subject.
| Registration | Target relationship | Normal cleanup owner |
|---|---|---|
| lv_subject_add_observer | No widget target association | Application removes observer, or retires subject |
| lv_subject_add_observer_with_target | Arbitrary borrowed target pointer | Application removes before target retirement, or retires subject first |
| lv_subject_add_observer_obj | Explicit widget association | Widget deletion, documented widget detach, or subject deinit |
For the hypothetical fixture, subject S holds quality value 10. Observer W updates a card widget. Observer N records the value in an application-owned diagnostic context. Keep S, both callback contexts and the card valid before registering either observer.
The configuration template names LV_USE_OBSERVER. Save the effective configuration and exact tag with the fixture. A documentation page’s current feature list is not sufficient evidence that a helper exists in this specific release.
2. Expect notification during registration
The tagged registration implementations invoke the supplied callback immediately before returning the newly allocated observer. Initialize the target and user data before registration. A callback must not depend on the caller having already assigned the returned handle into an application field.
Use the callback’s observer argument to obtain its target or user data. For the first fixture, keep callbacks simple and prohibit registration-time teardown or self-removal. More complicated reentrancy deserves its own source audit rather than being inferred from successful ordinary notification.
Check a null registration result before treating the subscription as active. Keep readiness separate from callback counts: the first callback establishes the initial view; it does not prove an external producer has supplied a new measurement.
For integer subjects, setting the same value does not notify again through the ordinary changed-value path. Thus the fixture expects one callback at registration, a second when changing 10 to 11, and no additional callback when setting 11 again. Explicit notify is a different operation and is excluded from that count rule.
3. Let the widget association do its job
The public header documents automatic removal for lv_subject_add_observer_obj when the widget is deleted. The implementation installs an LV_EVENT_DELETE hook carrying the observer. This is why using the widget-specific registration matters; storing a widget pointer in generic user data does not install that hook.
After deleting W’s target, discard any saved W observer handle. Automatic removal frees the observer, so a later manual removal through the old handle would be a use-after-free rather than extra cleanup.
If the widget remains but changes models, use the documented lv_obj_remove_from_subject(widget, subject) operation to detach the matching association before rebinding. Its NULL-subject form removes associations across subjects. Avoid direct removal of widget-associated handles in this recipe: the tagged header cautions against it, although the implementation and broader guide describe more permissive removal behavior.
Do not remove the widget’s lifecycle callback indiscriminately while leaving the observer subscribed. Component cleanup that erases every event callback can defeat the association on which automatic retirement depends. Review event-removal helpers alongside subscription creation.
4. Retire non-widget targets explicitly
Observer N has no widget deletion event. Before freeing its diagnostic context, call lv_observer_remove on the still-live N handle, then clear the application’s handle field. Keep the subject valid during that call because removal accesses the subject’s observer list.
Passing a pointer through the target or user-data parameter does not transfer ownership. The private observer structure contains an internal auto-free-user-data flag, and built-in bindings may use it for their own allocations. Ordinary custom registrations in this fixture do not authorize LVGL to free the diagnostic context. Release it through its application owner after detaching.
If a screen disappears while N belongs to a longer-lived service, leaving N attached may be intentional. Its callback must then use service-owned data and remain independent of the deleted screen. The right lifetime follows the actual target, not the location where registration code was first called.
5. Treat subject deinit as disconnection
The deinit contract removes observers and associated widget-binding events. In the exact implementation it walks and removes subscriptions; it does not free the lv_subject_t storage, delete the target widgets or publish a final value to callbacks.
Do not assume it marks the subject’s type invalid. The pinned function does not establish that tombstone. Stop producers and clear application references before retiring the containing model. Reinitializing a live subject without first disconnecting it is also not a cleanup operation: initialization resets its fields and list state.
When S is retired first, W and N observer handles both become stale. Clear those handles as part of the model retirement transaction; do not later remove N a second time. The surviving card can be explicitly bound to a newly initialized subject after the old route has been disconnected.
6. Verify both retirement orders
In branch A, register W and N at value 10, change to 11, repeat 11, delete the card, then change S to 12. Expected cumulative callback counts are W=2 and N=3. Remove N while S remains live, retire its context, and finally deinitialize S.
In branch B, start fresh and register both observers, then deinitialize S while both targets still exist. Expect no extra value callbacks from deinit. Clear both handles, delete the surviving card and release N’s context. Use allocation diagnostics to detect a leftover delete hook or double removal.
Repeat with explicit widget detachment and rebinding to another subject. Record subscription identity, target identity, callback cause and release owner. Accept the binding layer only when both target-first and subject-first retirement finish without callbacks into retired storage or duplicate subscription cleanup.