Skip to main content

flutter_message_composer 0.5.2 copy "flutter_message_composer: ^0.5.2" to clipboard
flutter_message_composer: ^0.5.2 copied to clipboard

An adaptive Flutter message composer with multiline text, attachments, audio previews, and hold-to-record gestures with slide-to-cancel and locking.

flutter_message_composer #

An adaptive Flutter message composer for chat and messaging experiences. It provides multiline text input, configurable actions, typed submissions, and tap-or-hold audio recording with playback previews. It is independent of networking, storage, and state management.

The package targets Android and iOS for voice-note recording, with native emoji input on iOS, Android, and macOS.

Features #

  • Compact input that animates into a multiline composer.
  • Configurable attachment menu and prompt actions.
  • Text, attachment, and recorded-audio submission payloads.
  • Tap to manage a recording or hold to record and send on release.
  • Slide toward cancel to discard a held recording, or upward to lock it and continue hands-free without sending on release.
  • Stop, play, pause, resume, and replay audio before sending.
  • Tabular MM:SS recording and playback times beside the waveform.
  • Playback progress and theme-aware recording-lock feedback.
  • Disabled and sending states.
  • Theme-aware defaults with optional colors, shapes, icons, and tooltips.
  • A customizable suggestion area for mentions, hashtags, commands, quick replies, or any other contextual content.
  • Callbacks for typing, recording lifecycle, permission denial, and errors.
  • Async hook before microphone startup for host-owned sounds or telemetry.
  • Typed recording lifecycle events for host-owned orchestration.
  • A controller for triggering actions from external buttons and observing their availability without accessing widget state.
  • Emoji action visible by default, with native input and an optional custom-picker callback.
  • An injectable recorder contract for custom native or DSP-backed capture.
  • A lazily created, injectable audio preview player.
  • No dependency on a chat backend or state-management package.

Installation #

Add the published package to pubspec.yaml:

dependencies:
  flutter_message_composer: ^0.5.2

Platform setup #

Requires Dart 3.13.4 or newer and Flutter 3.44.0 or newer. Dependency resolution also requires a Flutter SDK that includes the requested Dart version.

Audio recording uses the flutter_recorder package. The default recorder writes voice-note friendly mono WAV files at 22.05 kHz with 16-bit PCM samples, matching flutter_recorder's native defaults. On iOS it uses the generic PlayAndRecord preset so microphone startup is not left behind a playback-only audio session. Native silence detection is available through MessageComposerAudioRecorderConfig, but it is opt-in because flutter_recorder requires PCMFormat.f32le for silence detection while 16-bit PCM WAV is the safest default for local playback.

Android requires API 24 or newer, compileSdk 37 or newer, and the microphone permission in android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

iOS requires version 15.0 or newer and a usage description in ios/Runner/Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>Audio messages require access to the microphone.</string>

Usage #

import 'package:flutter/material.dart';
import 'package:flutter_message_composer/flutter_message_composer.dart';

final controller = TextEditingController();
final focusNode = FocusNode();

MessageComposer(
  controller: controller,
  focusNode: focusNode,
  hintText: 'Message',
  sendTooltip: 'Send',
  attachTooltip: 'Attachments',
  recordTooltip: 'Record audio',
  suggestions: activeSuggestions.isEmpty
      ? null
      : SuggestionsList(
          suggestions: activeSuggestions,
          onSelected: handleSuggestion,
        ),
  menuActions: [
    ChatInputMenuAction(
      id: 'photo',
      label: 'Photo',
      icon: const Icon(Icons.image_outlined),
      onPressed: pickPhoto,
    ),
  ],
  menuPadding: const EdgeInsets.all(6),
  attachments: selectedAttachments,
  onChanged: handleTyping,
  onSend: (submission) {
    if (submission.hasAudio) {
      uploadAndSendAudio(submission.audio!);
      return;
    }

    sendMessage(
      text: submission.text,
      attachments: submission.attachments,
    );
  },
)

The suggestions property accepts a widget for displaying and selecting suggestions. Pass null when none are active and the area collapses automatically. The controller property accepts any TextEditingController, including specialized token-aware controllers. No text parser is required by this package.

The caller owns attachment picking, uploads, message creation, retries, and clearing the text controller after a submission is accepted. Pass sending: true to block additional submissions, attachments and recordings while an operation is in progress. The text field stays enabled, focused and editable so the user can prepare the next draft without reopening the keyboard. Use enabled: false when the entire composer must be disabled, such as when write access is revoked. Send controls and IME submission preserve text focus; completion does not request focus after the user has moved elsewhere.

When clearing after an asynchronous submission, only clear if the controller still contains the submitted draft. Do not discard text entered while waiting for the previous operation to complete.

The default recorder uses flutter_recorder. To customize capture format, silence handling, input presets, or audio processing, provide a MessageComposerAudioRecorderConfig or implement MessageComposerRecorder and return an isolated instance with audioRecorderFactory. The composer owns and disposes the returned backend.

MessageComposerAudioRecorderConfig.bestCompatible is the package default and records WAV files, which is the safest option for local preview/playback and services that accept standard audio uploads. Services that prefer smaller compressed voice notes can opt into Ogg Opus:

MessageComposer(
  controller: controller,
  focusNode: focusNode,
  audioRecorderFactory: () => MessageComposerAudioRecorder(
    config: MessageComposerAudioRecorderConfig.compressedOpus,
  ),
  onSend: handleSubmission,
)

Use onBeforeStartRecording for host-owned work that must finish before the microphone opens, such as playing a mic-opened sound. Use onRecordingEvent to react to typed phases such as permission checking, preparing, recording, locked, stopping, stopped, canceling, canceled, sending, sent, and failed.

External actions #

Attach a caller-owned MessageComposerController with actionsController. It controls the existing composer, including its permission checks and callbacks:

final actions = MessageComposerController();

MessageComposer(
  controller: textController,
  focusNode: focusNode,
  actionsController: actions,
  hintText: 'Message',
  onSend: handleSubmission,
);

// A host can set its reply target before starting hands-free capture.
setReplyTarget(selectedMessage);
final started = await actions.startRecording();

startRecording() returns true after microphone capture starts. stopRecording() prepares a preview, cancelRecording() discards audio or queues cancellation during startup, and send() submits the current text/attachments or audio. toggleAudioPreview(), showAttachmentMenu(), hideAttachmentMenu(), toggleAttachmentMenu(), focusTextInput(), showEmojiKeyboard(), and toggleEmojiKeyboard() are also available. Commands return false when unavailable, disabled, busy, or detached. Reply metadata belongs to the host and is not included in the package's payload.

Observe actions.value with ValueListenableBuilder<MessageComposerValue> to enable external controls according to canStartRecording, canSend, canShowEmojiKeyboard, isEmojiKeyboardOpen, canShowAttachmentMenu, isBusy, recordingStatus, or isPreviewPlaying. One controller can attach to only one composer at a time. The caller disposes it. Replacing or disposing a controller prevents further commands but does not discard audio already owned by a mounted composer.

Emoji action #

The emoji action is visible and functional by default on iOS, Android and macOS. No app-owned picker is required. Set showEmojiButton: false to hide it, or enableEmojiButton: false to disable requests independently from visibility. On mobile, the action shows an emoji icon during text input and a keyboard icon while its native emoji input is open. Tapping it switches back without discarding the draft. Customize emojiButtonIcon / emojiTooltip and textKeyboardButtonIcon / textKeyboardTooltip as needed. Both states preserve the same action size and buttonSpacing, including on narrow layouts.

MessageComposer(
  controller: textController,
  focusNode: focusNode,
  hintText: 'Message',
  enableEmojiButton: canUseEmojis,
  emojiTooltip: 'Emojis',
  textKeyboardTooltip: 'Keyboard',
  onEmojiError: handlePickerError,
  onSend: handleSubmission,
)

Default native behavior:

  • iOS: opens Apple's Emoji keyboard using a complete UIKit text-input client, including system Emoji search on supported iOS versions. Emoji must be enabled in Settings > General > Keyboard > Keyboards. If unavailable, the composer restores the normal text field and reports onEmojiError. Swift Package Manager and CocoaPods are supported. UIKit input-mode changes update the action within the package's native session, including Emoji search and the ABC control. Switches made directly from Flutter's regular keyboard are not tracked: some iOS versions report the previous language instead of Emoji and provide no mode in the notification.
  • Android: opens Google's native AndroidX Emoji Picker with recent emojis and skin-tone variants. This is an included native view, not a command to switch Gboard, Samsung Keyboard or another installed IME to its emoji tab. AndroidX provides categories and recent emojis, not a text-search field. The action tracks this picker's visibility, not internal tabs of an installed IME.
  • macOS: opens the system emoji/character palette for the focused field.

Native input preserves the draft and selection. Backspace removes complete emoji sequences, including skin-tone variants and joined emoji. Mobile sessions are closed when their composer is disposed, disabled or starts recording. Returning to typing restores the normal field. A send in progress does not discard the next draft.

For an optional custom picker, set onEmojiPressed: openEmojiPicker. It replaces native presentation and owns both presentation and insertion. Platforms without native support also accept this callback. No fallback picker is silently used. For an inline custom picker, supply isEmojiPickerOpen and onTextKeyboardPressed so the action can reflect its visibility and close it before returning to text input.

MessageComposerEmojiKeyboard.isSupported reports platform support. The action controller's showEmojiKeyboard() manages focus and lifecycle automatically. For standalone fields, MessageComposerEmojiKeyboard.show(controller: ...) requires a destination controller on mobile; unfocus the ordinary field first and call dismiss() before disposing that controller. macOS can use show() without a controller because it inserts through the already-focused field. focusTextInput() restores the regular text field; the system determines its selected keyboard layout. There is no universal detection of mode changes made outside the package's native input session.

Audio workflows #

  • Tap the microphone: record until Stop, Send, or Cancel is pressed.
  • Hold the microphone: record and send once when the same finger releases.
  • Slide toward Cancel: discard immediately after the horizontal threshold. This direction is left in left-to-right layouts and right in right-to-left layouts, matching the cancel button.
  • Slide upward: lock recording. Releasing the finger does not send; Stop, Send, and Cancel remain available.
  • Stop: show Play and the recording's total duration.
  • Play / Pause: listen before sending, resume from the paused position, or replay from the start after completion. Playback never submits audio.

The time before the waveform is elapsed recording time, current playback time while playing or paused, and total duration while stopped or completed. Minutes and seconds have at least two digits (01:03); recordings longer than an hour keep their total minutes (61:03).

Sending or canceling stops the preview first. Canceling or removing the composer discards its unsent recording. Once onSend receives an audio submission, the callback recipient owns that file and is responsible for its eventual cleanup. Releasing while permission or microphone startup is pending preserves the user's send, cancel, or lock intent without duplicate submissions.

Customization #

The new options are optional. Existing MessageComposer calls gain previews and recording gestures without additional setup.

MessageComposer(
  controller: controller,
  focusNode: focusNode,
  hintText: 'Message',
  recordingCancelDragDistance: 96,
  recordingLockDragDistance: 80,
  enableRecordingHaptics: true,
  recordingTimerStyle: const TextStyle(fontWeight: FontWeight.w600),
  cancelRecordingTooltip: 'Cancel',
  stopRecordingTooltip: 'Stop recording',
  playRecordingTooltip: 'Play audio',
  pauseRecordingTooltip: 'Pause audio',
  sendRecordingTooltip: 'Send audio',
  lockRecordingLabel: 'Lock recording',
  recordingLockedLabel: 'Recording locked',
  onRecordingLocked: handleRecordingLocked,
  onAudioPreviewError: handlePreviewError,
  onSend: handleSubmission,
)

Use playRecordingButtonIcon and pauseRecordingButtonIcon to replace the default icons, alongside the existing cancel, stop, and send icon overrides. The timer style merges with the theme and tabular-digit defaults. Gesture distances are positive logical pixels. Light-impact feedback confirms that a held recording has started and when a cancel or lock threshold is reached. Start feedback waits for microphone permission and successful capture; it is not emitted for failed starts or recordings already queued to send or cancel. Haptics can be disabled with enableRecordingHaptics: false.

Silence detection is disabled by default. Enable it with MessageComposerAudioRecorderConfig, set pcmFormat: PCMFormat.f32le, and opt into pauseRecordingOnSilence only when the host app wants to skip silent segments while writing the file. Use the config to change the silence threshold, required silent duration, pre-roll, recording format, sample rate, channel count, or platform input preset. Setting enableRecordingHaptics: false disables the composer's gesture feedback.

Previews use audioplayers. For another backend, implement MessageComposerPlayer and supply audioPlayerFactory. Report position changes and playback states, including pauses caused by native interruptions and completion. The composer owns the player but creates it only when the first preview is requested. onAudioPreviewError handles preview failures separately from capture errors and leaves the audio available to retry, send, or discard.

Recording labels have English defaults. Set the label and tooltip parameters to provide localized text. See example/lib/main.dart for a complete usage example.

Scope #

flutter_message_composer is a presentation component. It does not prescribe a message model, networking client, storage layer, or state-management solution. Submissions and lifecycle callbacks provide the integration points.

1
likes
160
points
666
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

An adaptive Flutter message composer with multiline text, attachments, audio previews, and hold-to-record gestures with slide-to-cancel and locking.

Homepage

Topics

#chat #messaging #composer #audio-recording #user-interface

License

MIT (license)

Dependencies

audioplayers, flutter, flutter_recorder, hugeicons, path_provider, permission_handler

More

Packages that depend on flutter_message_composer

Packages that implement flutter_message_composer