# 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](/chat/docs/sdk/flutter/stream-chat-flutter/message-composer/stream-message-composer/).
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.

```dart
StreamMessageComposer(
  allowedAttachmentPickerTypes: [
    AttachmentPickerType.images,
    AttachmentPickerType.videos,
  ],
)
```

To disable the attachment picker entirely, pass an empty list:

```dart
StreamMessageComposer(
  allowedAttachmentPickerTypes: [],
)
```

### Attachment size and count limits

File size limits, allowed/blocked extensions, and allowed/blocked MIME types are configured in the [Stream Dashboard](https://dashboard.getstream.io/) 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):

```dart
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:**

```dart
StreamMessageComposer(
  attachmentPickerOptionsBuilder: (context, defaultOptions) {
    return defaultOptions.where((o) => o.key != 'poll-creator').toList();
  },
)
```

**Reorder tabs:**

```dart
StreamMessageComposer(
  attachmentPickerOptionsBuilder: (context, defaultOptions) {
    return defaultOptions.reversed.toList();
  },
)
```

**Add a custom tab:**

```dart
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](/chat/docs/sdk/flutter/ui/message-composer/system-attachments-picker/) for details.

```dart
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`:

```dart
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:

```dart
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:**

```dart
class LocationPicked extends CustomAttachmentPickerResult {
  const LocationPicked({required this.location});
  final LatLng location;
}
```

**Step 2 - Add a custom picker tab that emits the result:**

```dart
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`:**

```dart
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:

```dart
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:

```dart
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](/chat/docs/sdk/flutter/stream-chat-flutter/customizing-widgets/) 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`:

```dart
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](/chat/docs/sdk/flutter/stream-chat-flutter/message-list/stream-message-item/) 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`.


---

This page was last updated at 2026-06-09T15:44:07.750Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/flutter/stream-chat-flutter/message-composer/attachments/](https://getstream.io/chat/docs/sdk/flutter/stream-chat-flutter/message-composer/attachments/).