Typing Indicators

The Stream Flutter SDK displays typing indicators automatically when other channel members are composing a message. The indicator updates in real time as users start and stop typing.

Default behavior

StreamChannelHeader shows a typing indicator in its subtitle by default via StreamChannelInfo. When one or more other users are typing, the subtitle shows their names and an animated dots icon. No configuration is required to enable this.

To show a typing indicator above the composer, add StreamTypingIndicator to StreamMessageListView's builders.header slot — it renders at the bottom of the list (the default list is reversed, so the header builder paints below the most recent message):

StreamMessageListView(
  builders: StreamMessageListViewBuilders(
    header: (context) => Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      child: StreamTypingIndicator(),
    ),
  ),
)

Theming

StreamTypingIndicator accepts a style parameter that controls the text appearance:

StreamTypingIndicator(
  style: const TextStyle(
    color: Colors.grey,
    fontStyle: FontStyle.italic,
  ),
)

The typing indicator shown in StreamChannelHeader is rendered by StreamChannelInfo. Pass a textStyle to it via a custom subtitle to change the text appearance:

StreamChannelHeader(
  subtitle: StreamChannelInfo(
    channel: StreamChannel.of(context).channel,
    textStyle: const TextStyle(
      color: Colors.grey,
      fontStyle: FontStyle.italic,
    ),
  ),
)

Reading the typing state

Typing state is available from the Channel object provided by StreamChannel:

final typingUsers = StreamChannel.of(context).channel.typingEvents.keys.toList();

typingEvents is a Map<User, Event> of members currently composing a message (excluding the current user). Use .keys.toList() to get a List<User>.

For reactive updates, listen to the stream instead:

StreamBuilder<Map<User, Event>>(
  stream: StreamChannel.of(context).channel.typingEventsStream,
  builder: (context, snapshot) {
    final typingUsers = snapshot.data?.keys.toList() ?? [];
    if (typingUsers.isEmpty) return const SizedBox.shrink();
    final names = typingUsers.map((u) => u.name).join(', ');
    return Text('$names is typing…');
  },
)

Controlling the typing event from the composer

StreamMessageComposer automatically sends start-typing and stop-typing events as the user types. If you build a fully custom composer, send these events manually using the channel API.

There are two approaches:

channel.keyStroke() — call this on every keypress. The SDK sends a typing.start event and then automatically schedules a typing.stop event after a debounce period. This is the recommended approach for most custom composers:

TextField(
  onChanged: (_) => channel.keyStroke(),
)

channel.startTyping() / channel.stopTyping() — send raw typing.start and typing.stop events directly, giving you full manual control. Use these only when you need to drive the typing lifecycle yourself (for example, a voice input that starts and stops on button press):

// When the user begins composing
await channel.startTyping();

// When the user finishes or cancels
await channel.stopTyping();

For most cases, keyStroke() is the simpler choice because the SDK handles the typing.stop automatically.