Repository navigation
docs(ai): say conversation builders read changing state through a Listenable (FLU-1107) - #34
Conversation
…tenable (FLU-1107) AIConversationView caches its rows, so a builder that captures state of its own, such as a reply's rating, never sees it change, and builders created in build rebuild every row. Say so in the AIConversationView and AIComponentBuilders docs and the README, as iOS does, and fix the README's replyActions example, whose thumbs never filled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xsahil03x
left a comment
There was a problem hiding this comment.
Thanks for writing this down. The row cache in _EntryViewState is easy to trip over, and the old README example was exactly the trap: a captured ratings[...] never refreshes. I checked the new wording against the code with a throwaway widget test: with stable builders a captured value stays stale, a ValueListenableBuilder inside the builder updates, and new AIComponentBuilders on each host rebuild call the row builders again while the same instance doesn't. It also matches iOS's makeReplyActionsView doc. dart analyze --fatal-infos is clean and flutter test --exclude-tags golden passes.
The rest is optional and inline.
| rating: ratings[props.message.id], | ||
| onRate: (rating) => rate(props.message, rating), | ||
| rating: rated[props.message.id], | ||
| onRate: (rating) => rate(props.message, rating), // updates `ratings` |
There was a problem hiding this comment.
Optional: show how rate updates ratings. A reader who writes ratings.value[id] = rating changes the map in place, ValueNotifier doesn't notify, and the thumbs-up still doesn't fill, which is the bug this section is warning about.
| onRate: (rating) => rate(props.message, rating), // updates `ratings` | |
| onRate: (rating) => rate(props.message, rating), // sets `ratings.value` to a new map |
| /// text: props.text, | ||
| /// actions: AIReplyActions( | ||
| /// rating: rated[props.message.id], | ||
| /// onRate: (rating) => rate(props.message, rating), // updates `ratings` |
There was a problem hiding this comment.
Optional: same as in the README. Assigning a new map is what makes the ValueListenableBuilder rebuild.
| /// onRate: (rating) => rate(props.message, rating), // updates `ratings` | |
| /// onRate: (rating) => rate(props.message, rating), // sets `ratings.value` to a new map |
| - Create the builders once, such as in a `late final` field, not in `build`: new builders rebuild | ||
| every row whenever your widget rebuilds, such as on every chunk of a streaming reply. |
There was a problem hiding this comment.
Optional: the same applies to onAttachmentTap and approver. _Shown compares both, so an onAttachmentTap: (a) => ... closure written in build rebuilds every row on each host rebuild, just like inline builders. I checked this with a test. The AIConversationView class doc could say the same.
| - Create the builders once, such as in a `late final` field, not in `build`: new builders rebuild | |
| every row whenever your widget rebuilds, such as on every chunk of a streaming reply. | |
| - Create the builders once, such as in a `late final` field, not in `build`: new builders rebuild | |
| every row whenever your widget rebuilds, such as on every chunk of a streaming reply. The same goes | |
| for the `onAttachmentTap` and `approver` you give `AIConversationView`. |
There was a problem hiding this comment.
Done in 3b7168e, in the README and the AIConversationView class doc.
| /// you set it. A row is only built again when what its message says changes, | ||
| /// or the builders do, so a reply streaming in does not rebuild the rest of the | ||
| /// conversation. A builder that shows state of your own, such as a reply's | ||
| /// rating, must therefore read it through a [Listenable] (a |
There was a problem hiding this comment.
nit: "must" says more than the code does. The row clears its cache in didChangeDependencies, so a builder that looks up an InheritedWidget through the context it gets (Theme.of, provider's context.watch) also rebuilds when that widget changes. I checked this with a test. "should" keeps the advice without telling Provider users their setup is wrong.
| /// rating, must therefore read it through a [Listenable] (a | |
| /// rating, should therefore read it through a [Listenable] (a |
Notes that onAttachmentTap and approver rebuild every row like inline builders, and that rate must assign a new map for the notifier to fire. A builder reading an InheritedWidget through its context also updates, so reading state through a Listenable is advice, not a requirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linear: FLU-1107
Docs only, no behaviour change. This aligns with iOS, which documents the same caching on
makeReplyActionsView.Problem
AIConversationViewcaches each row. A row is built again only when what its message says changes, or when theAIComponentFactorybuilders change. State the host keeps itself, such as a reply's rating, isn't part of that:replyActionsbuilder that capturesratings[...]directly never sees the rating change, so a tapped 👍 doesn't fill.build, as the README example did, every row rebuilds on every streamed chunk.The same applies to custom
userMessage/assistantMessagebuilders that read host state. Neither rule was documented, and the README'sreplyActionsexample capturedratings[props.message.id]directly.Change
AIConversationView's class doc says rows are cached, that a builder showing host state reads it through aListenable(ListenableBuilder/ValueListenableBuilder), and that builders should be created once.AIComponentBuilders.userMessage/assistantMessage/replyActionssay the same.replyActionsgets an example that reads the rating through aValueListenableBuilderfrom alate finalbuilders field.replyActionsexample rewritten in the same pattern (aValueNotifierof ratings, builders created once).melos run lint:allpasses.CI=true melos run test:all: everything passes except theAIToolApprovalViewandStreamingReasoningViewCI goldens, which fail the same way onmainwhen run on macOS (they're rendered on Linux).🤖 Generated with Claude Code