StreamMessageComposer(
allowedAttachmentPickerTypes: [
AttachmentPickerType.images,
AttachmentPickerType.videos,
],
)Attachments
Overview
The Stream Chat Flutter SDK has built-in support for several attachment types that users can send in messages. On mobile, the attachment picker appears as an inline panel below the text input when the user taps the attachment button in StreamMessageComposer. On web and desktop, a system list picker is shown instead.
Built-in attachment types are represented by AttachmentPickerType:
| Type | Description |
|---|---|
AttachmentPickerType.images | Photos from the device gallery |
AttachmentPickerType.videos | Videos from the device gallery |
AttachmentPickerType.audios | Audio files from the device |
AttachmentPickerType.files | Any file from the device file browser |
AttachmentPickerType.poll | Create a poll |
Restricting allowed attachment types
Use allowedAttachmentPickerTypes on StreamMessageComposer to limit which attachment types users can select.
Only the specified types appear in the attachment picker.
To disable the attachment picker entirely, pass an empty list:
StreamMessageComposer(
allowedAttachmentPickerTypes: [],
)Attachment size and count limits
File size limits, allowed/blocked extensions, and allowed/blocked MIME types are configured in the Stream Dashboard and enforced automatically by StreamMessageComposer via StreamChatClient.appSettings.
The SDK fetches AppSettings on connect and caches them. To refresh manually, call client.getAppSettings().
The per-message attachment count limit is controlled by the attachmentLimit parameter (defaults to 30, the backend cap):
StreamMessageComposer(
attachmentLimit: 10,
)Customizing picker options
Use attachmentPickerOptionsBuilder to modify, filter, reorder, or extend the default options shown in the picker.
Filter out specific tabs:
StreamMessageComposer(
attachmentPickerOptionsBuilder: (context, defaultOptions) {
return defaultOptions.where((o) => o.key != 'poll-creator').toList();
},
)Reorder tabs:
StreamMessageComposer(
attachmentPickerOptionsBuilder: (context, defaultOptions) {
return defaultOptions.reversed.toList();
},
)Add a custom tab:
StreamMessageComposer(
attachmentPickerOptionsBuilder: (context, defaultOptions) {
return [
...defaultOptions,
TabbedAttachmentPickerOption(
key: 'audio-picker',
icon: Icons.audiotrack,
supportedTypes: [AttachmentPickerType.audios],
optionViewBuilder: (context, controller) {
return AudioPicker(
onAudioPicked: (audio) async {
await controller.addAttachment(audio);
},
);
},
),
];
},
)Option types
| Type | Description |
|---|---|
TabbedAttachmentPickerOption | Custom inline UI shown as a tab (gallery, custom pickers) |
SystemAttachmentPickerOption | Launches a native system dialog (file browser, camera) |
Both types take icon: IconData (not a Widget).
Using the system picker on mobile
Set useSystemAttachmentPicker: true to use the system list picker on mobile instead of the custom tabbed picker.
This is useful for compliance with Google Play's photo and video permissions policy.
See System Attachments Picker for details.
StreamMessageComposer(
useSystemAttachmentPicker: true,
)Gallery thumbnail configuration
Thumbnail appearance in the gallery tab is configured via GalleryPickerConfig, passed when building a custom picker via tabbedAttachmentPickerBuilder or systemAttachmentPickerBuilder.
| Field | Default | Description |
|---|---|---|
mediaThumbnailSize | null | null; when null, each tile auto-calculates its size from its layout constraints and the device pixel ratio |
mediaThumbnailFormat | ThumbnailFormat.jpeg | ThumbnailFormat.jpeg or ThumbnailFormat.png |
mediaThumbnailQuality | 100 | Quality value between 0-100 |
mediaThumbnailScale | 1 | Logical-pixel scale factor |
Handling attachment errors
Attachment errors are strongly typed. Handle them via the onError callback on StreamMessageComposer:
StreamMessageComposer(
onError: (error, stackTrace) {
if (error is AttachmentTooLargeError) {
showSnackBar('File too large');
} else if (error is AttachmentLimitReachedError) {
showSnackBar('Too many attachments');
} else if (error is AttachmentBlockedError) {
showSnackBar('File type not allowed');
}
},
)The three typed errors correspond to the validation rules enforced by AppSettings:
| Error | Cause |
|---|---|
AttachmentTooLargeError | File exceeds the size limit configured in the Stream Dashboard |
AttachmentLimitReachedError | Number of attachments exceeds attachmentLimit |
AttachmentBlockedError | File extension or MIME type is blocked in the Stream Dashboard upload config |
You can also catch errors directly from StreamAttachmentPickerController.addAttachment() inside a custom picker tab:
try {
await controller.addAttachment(attachment);
} on AttachmentTooLargeError catch (e) {
showError('File too large: ${e.fileSize} bytes (max ${e.maxSize} bytes)');
} on AttachmentLimitReachedError catch (e) {
showError('Maximum ${e.maxCount} attachments allowed');
} on AttachmentBlockedError catch (e) {
showError('File type not allowed: ${e.fileExtension ?? e.mimeType}');
}Custom attachment results
For custom picker tabs that produce non-media results (for example, location or contact), use controller.notifyCustomResult() instead of addAttachment().
This emits a CustomAttachmentPickerResult that StreamMessageComposer forwards to onAttachmentPickerResult.
Do not call Navigator.pop() from inside a picker tab. The picker is an inline widget, not a modal route, and popping would close the wrong page.
notifyCustomResult is the correct way to signal completion.
Step 1 - Define your result type:
class LocationPicked extends CustomAttachmentPickerResult {
const LocationPicked({required this.location});
final LatLng location;
}Step 2 - Add a custom picker tab that emits the result:
StreamMessageComposer(
attachmentPickerOptionsBuilder: (context, defaultOptions) {
return [
...defaultOptions,
TabbedAttachmentPickerOption(
key: 'location-picker',
icon: Icons.location_on,
supportedTypes: [CustomLocationPickerType()],
optionViewBuilder: (context, controller) {
return LocationPicker(
onLocationPicked: (location) {
if (location == null) return; // user cancelled
controller.notifyCustomResult(LocationPicked(location: location));
},
);
},
),
];
},
)Step 3 - Handle the result in onAttachmentPickerResult:
StreamMessageComposer(
onAttachmentPickerResult: (result) {
if (result is LocationPicked) {
sendLocationMessage(result.location);
return true; // handled — StreamMessageComposer closes the picker
}
return false; // not handled — let StreamMessageComposer deal with it
},
)Return true to signal you handled the result (the picker closes automatically). Return false to let StreamMessageComposer handle it.
onAttachmentPickerResult is only called for CustomAttachmentPickerResult subtypes.
AttachmentsPicked, PollCreated, and AttachmentPickerError are handled internally by StreamMessageComposer.
Previewing custom attachment types in the composer
The notifyCustomResult pattern above emits a one-shot result that bypasses the attachment pipeline. It's useful when the developer wants to send a fully custom message themselves.
The alternative is to add a real Attachment with a custom type via controller.addAttachment(). The attachment then flows through the composer's preview area and is included on the sent message like any built-in attachment:
TabbedAttachmentPickerOption(
key: 'location-picker',
icon: Icons.location_on,
supportedTypes: [CustomLocationPickerType()],
optionViewBuilder: (context, controller) {
return LocationPicker(
onLocationPicked: (location) async {
if (location == null) return;
await controller.addAttachment(
Attachment(
type: 'location',
extraData: {
'file_size': 0,
'latitude': location.coordinates.latitude,
'longitude': location.coordinates.longitude,
},
),
);
},
);
},
)The default composer preview renders only the built-in attachment types (file, audio, voice recording, image, video, giphy). Any other type falls back to a generic "unsupported attachment" placeholder.
To render your own preview, register a messageComposerAttachment builder via StreamComponentFactory.
The builder is called for every attachment, so dispatch on the type and delegate to DefaultMessageComposerAttachment for the types you do not handle:
StreamChat(
client: client,
componentBuilders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
messageComposerAttachment: (context, props) {
if (props.attachment.type == 'location') {
return LocationAttachmentPreview(
attachment: props.attachment,
onRemovePressed: () => props.onRemovePressed?.call(props.attachment),
);
}
return DefaultMessageComposerAttachment(props: props);
},
),
),
child: MyApp(),
)See Customizing UI Components for more details on StreamComponentFactory.
Rendering custom attachment types in the message list
Once a message with a custom attachment is sent, the message list also needs to know how to render it.
Create a subclass of StreamAttachmentWidgetBuilder and register it via StreamChatConfigurationData.attachmentBuilders:
class LocationAttachmentBuilder extends StreamAttachmentWidgetBuilder {
@override
bool canHandle(Message message, Map<String, List<Attachment>> attachments) {
return attachments['location']?.isNotEmpty ?? false;
}
@override
Widget build(
BuildContext context,
Message message,
Map<String, List<Attachment>> attachments,
) {
final attachment = attachments['location']!.first;
return LocationMapWidget(
latitude: attachment.extraData['latitude']! as double,
longitude: attachment.extraData['longitude']! as double,
);
}
}
StreamChat(
client: client,
configData: StreamChatConfigurationData(
attachmentBuilders: [LocationAttachmentBuilder()],
),
child: MyApp(),
)Custom builders are prepended to the default builders, so they take priority for the attachment types they handle. The attachments map is keyed by Attachment.type, so attachments['location'] returns every location attachment on the message.
To scope the override to a single message, pass attachmentBuilders on StreamMessageItem (or StreamMessageItemProps.attachmentBuilders) directly.
See StreamMessageItem for additional examples and per-message overrides.
StreamAttachmentPickerController
The picker tracks selected attachments through StreamAttachmentPickerController.
The following methods are available inside an optionViewBuilder:
| Method | Description |
|---|---|
addAttachment(attachment) | Add a media attachment (image, video, file, audio) |
removeAttachment(attachment) | Remove a previously added attachment |
notifyCustomResult(result) | Emit a custom result (location, contact) to onAttachmentPickerResult |
StreamMessageComposer manages its own internal controller automatically.
You only need to create a StreamAttachmentPickerController manually if you are embedding StreamTabbedAttachmentPicker or StreamSystemAttachmentPicker directly outside of StreamMessageComposer.
- Overview
- Restricting allowed attachment types
- Attachment size and count limits
- Customizing picker options
- Using the system picker on mobile
- Gallery thumbnail configuration
- Handling attachment errors
- Custom attachment results
- Previewing custom attachment types in the composer
- Rendering custom attachment types in the message list
- StreamAttachmentPickerController