Skip to content

FlowAttachment models one attachment — its id, an optional thumbnail, the file kind shown in the type pill, and a label. FlowAttachmentGroup lays a set of them out for the composer or a message. A tile with an image opens the full-screen preview — zoom, paging — on tap; one without, a document, draws its ground and the pill and stays inert rather than opening an empty preview. Tiles stand at the design’s 80px on a 16px corner — 64 and 12 on phones, resolved from the theme’s platform.

The default layout is one row that scrolls, no scrollbar. With onRemove each tile grows a remove button — it fades in on hover and stays visible on touch:

The composer's pending row
FlowAttachmentGroup(
// The default: one row that scrolls, no scrollbar.
attachments: [
for (final picked in pending)
FlowAttachment(
id: picked.id,
thumbnail: FileImage(picked.file),
kind: picked.extension,
label: picked.name,
),
],
onRemove: (id) => detach(id),
removeTooltip: 'Remove attachment',
)

No thumbnail means no preview — the pill identifies the file:

Documents beside a photo
FlowAttachmentGroup(
layout: FlowAttachmentLayout.wrap,
attachments: [
// No thumbnail: the tile draws its ground and the type pill, and
// stays inert rather than opening an empty preview.
FlowAttachment(id: 'c', kind: 'PDF', label: 'contract.pdf'),
FlowAttachment(id: 'n', kind: 'DOCX', label: 'notes.docx'),
FlowAttachment(
id: 'd',
thumbnail: AssetImage('assets/dusk.png'),
kind: 'PNG',
label: 'dusk-ridge.png',
),
],
)

Without an onRemove callback the tiles are read-only. Tapping an image still opens the preview — pass onTap only to replace that, and call showFlowAttachmentPreview yourself if you want the preview alongside your own handling:

Wrap layout, no remove
FlowAttachmentGroup(
layout: FlowAttachmentLayout.wrap,
// No onRemove → read-only. Tapping still opens the preview: pass onTap
// only to replace it, and call showFlowAttachmentPreview yourself to
// keep it alongside your own handling.
attachments: sent,
)

Attachments travel as message parts: FlowAttachmentPart in FlowMessageData.parts. Nothing to wire — tapping a sent attachment opens the preview:

An attachment part
FlowMessage(
FlowMessageData(
id: 'm1',
role: FlowMessageRole.user,
parts: [
FlowAttachmentPart([
FlowAttachment(id: 'a', thumbnail: NetworkImage(url), kind: 'JPG'),
]),
FlowTextPart('What is the peak on the left?'),
],
),
// Nothing to wire: tapping a sent attachment opens the preview.
)

The composer renders the pending row above its field from the same model:

Pending attachments
FlowComposer(
attachments: attachments,
onRemoveAttachment: (id) => setState(
() => attachments = attachments.where((a) => a.id != id).toList(),
),
removeAttachmentTooltip: 'Remove attachment',
onSend: send,
leadingActions: [
FlowMenu(icon: Icons.add, entries: addOptions,
onSelected: pickAttachment),
],
)
  • FlowAttachmentid, thumbnail (any ImageProvider), preview (a full-resolution image for the viewer; defaults to thumbnail), kind, label, and tooltip (falls back to label).
  • FlowAttachmentGroupattachments, layout (FlowAttachmentLayout.scroll default / .wrap), onRemove, onTap, and the host-supplied tooltips, plus the tile metrics: size (80; 64 on phones), tileRadius (16; 12), spacing (8), padding. Callbacks report ids, never indexes, so ids must be unique within the group — asserted in debug.
  • showFlowAttachmentPreview — the full-screen viewer, callable directly when you override onTap or onAttachmentTap. Escape closes it and the arrow keys page.