Skip to content

docs(ai): say conversation builders read changing state through a Listenable (FLU-1107) - #34

Merged
VelikovPetar merged 2 commits into
mainfrom
petarvelikov/flu-1107-document-that-conversation-builders-must-read-changing-state
Oct 29, 2026
Merged

VelikovPetar merged 2 commits into
mainfrom
petarvelikov/flu-1107-document-that-conversation-builders-must-read-changing-state

Conversation

@VelikovPetar

Copy link
Copy Markdown
Contributor

Linear: FLU-1107

Docs only, no behaviour change. This aligns with iOS, which documents the same caching on makeReplyActionsView.

Problem

AIConversationView caches each row. A row is built again only when what its message says changes, or when the AIComponentFactory builders change. State the host keeps itself, such as a reply's rating, isn't part of that:

  • With stable builders, as the docs advise, a replyActions builder that captures ratings[...] directly never sees the rating change, so a tapped 👍 doesn't fill.
  • With builders created inline in build, as the README example did, every row rebuilds on every streamed chunk.

The same applies to custom userMessage / assistantMessage builders that read host state. Neither rule was documented, and the README's replyActions example captured ratings[props.message.id] directly.

Change

  • AIConversationView's class doc says rows are cached, that a builder showing host state reads it through a Listenable (ListenableBuilder / ValueListenableBuilder), and that builders should be created once.
  • AIComponentBuilders.userMessage / assistantMessage / replyActions say the same. replyActions gets an example that reads the rating through a ValueListenableBuilder from a late final builders field.
  • README (Conversations section): the two rules, and a replyActions example rewritten in the same pattern (a ValueNotifier of ratings, builders created once).
  • CHANGELOG: a 🔄 Changed line.

melos run lint:all passes. CI=true melos run test:all: everything passes except the AIToolApprovalView and StreamingReasoningView CI goldens, which fail the same way on main when run on macOS (they're rendered on Linux).

🤖 Generated with Claude Code

…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 xsahil03x left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
onRate: (rating) => rate(props.message, rating), // updates `ratings`
onRate: (rating) => rate(props.message, rating), // sets `ratings.value` to a new map

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3b7168e.

/// text: props.text,
/// actions: AIReplyActions(
/// rating: rated[props.message.id],
/// onRate: (rating) => rate(props.message, rating), // updates `ratings`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: same as in the README. Assigning a new map is what makes the ValueListenableBuilder rebuild.

Suggested change
/// onRate: (rating) => rate(props.message, rating), // updates `ratings`
/// onRate: (rating) => rate(props.message, rating), // sets `ratings.value` to a new map

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3b7168e.

Comment on lines +927 to +928
- 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- 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`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/// rating, must therefore read it through a [Listenable] (a
/// rating, should therefore read it through a [Listenable] (a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3b7168e.

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>
@VelikovPetar
VelikovPetar merged commit 952e806 into main Oct 29, 2026
6 checks passed
@VelikovPetar
VelikovPetar deleted the petarvelikov/flu-1107-document-that-conversation-builders-must-read-changing-state branch October 29, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants