From 8e735c4057072c333aeeef31e076d0889b231ccb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 26 Feb 2026 01:03:48 +0530 Subject: [PATCH 1/2] feat(ui): enhance StreamComponentBuilders with extension support Allow external packages to register typed component builders via StreamComponentBuilderExtension without modifying StreamComponentBuilders. Extensions are keyed by their Props type and retrieved through the extension() method, enabling per-component customization beyond the built-in builder set. Co-authored-by: Cursor --- .../src/factory/stream_component_factory.dart | 178 ++++++++++++++++-- .../stream_component_factory.g.theme.dart | 12 +- 2 files changed, 170 insertions(+), 20 deletions(-) diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart index 0bde159..2b69e18 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart @@ -10,7 +10,8 @@ part 'stream_component_factory.g.theme.dart'; /// Wrap a subtree with [StreamComponentFactory] to customize how Stream /// components are rendered. Access the builders using /// [StreamComponentFactory.of], [StreamComponentFactory.maybeOf], or the -/// [BuildContext] extension [StreamComponentFactoryExtension.streamComponentFactory]. +/// [BuildContext] extension +/// [StreamComponentFactoryExtension.streamComponentFactory]. /// /// The nearest [StreamComponentFactory] ancestor takes precedence - nested /// factories completely override their parents rather than merging. @@ -125,8 +126,7 @@ typedef StreamComponentBuilder = Widget Function(BuildContext context, T prop /// A collection of builders for customizing Stream component rendering. /// /// All builders are nullable - when null, the component uses its default -/// implementation. This follows the same pattern as Flutter's theme system -/// where widgets fall back to their defaults when theme values are null. +/// implementation. /// /// {@tool snippet} /// @@ -139,31 +139,112 @@ typedef StreamComponentBuilder = Widget Function(BuildContext context, T prop /// ``` /// {@end-tool} /// +/// {@tool snippet} +/// +/// Register a custom component builder via [extensions]: +/// +/// ```dart +/// final builders = StreamComponentBuilders( +/// extensions: [ +/// StreamComponentBuilderExtension( +/// builder: (context, props) => MyCustomMessage(props: props), +/// ), +/// ], +/// ); +/// ``` +/// +/// Then retrieve it with [extension]: +/// +/// ```dart +/// final builder = StreamComponentFactory.of(context).extension(); +/// if (builder != null) return builder(context, props); +/// ``` +/// {@end-tool} +/// /// See also: /// /// * [StreamComponentFactory], which provides builders to descendants. -@themeGen +/// * [StreamComponentBuilderExtension], which wraps a custom component builder. @immutable +@ThemeGen(constructor: 'raw') class StreamComponentBuilders with _$StreamComponentBuilders { /// Creates component builders with optional custom implementations. /// /// Any builder not provided (null) will cause the component to use its /// default implementation. - const StreamComponentBuilders({ - this.avatar, - this.avatarGroup, - this.avatarStack, - this.badgeCount, - this.button, - this.checkbox, - this.contextMenuAction, - this.emoji, - this.emojiButton, - this.fileTypeIcon, - this.onlineIndicator, - this.progressBar, + /// + /// [extensions] accepts an [Iterable] of [StreamComponentBuilderExtension] instances, which are + /// converted to a map keyed by [StreamComponentBuilderExtension.type] internally. + factory StreamComponentBuilders({ + StreamComponentBuilder? avatar, + StreamComponentBuilder? avatarGroup, + StreamComponentBuilder? avatarStack, + StreamComponentBuilder? badgeCount, + StreamComponentBuilder? button, + StreamComponentBuilder? checkbox, + StreamComponentBuilder? contextMenuAction, + StreamComponentBuilder? emoji, + StreamComponentBuilder? emojiButton, + StreamComponentBuilder? fileTypeIcon, + StreamComponentBuilder? onlineIndicator, + StreamComponentBuilder? progressBar, + Iterable>? extensions, + }) { + extensions ??= >[]; + + return .raw( + avatar: avatar, + avatarGroup: avatarGroup, + avatarStack: avatarStack, + badgeCount: badgeCount, + button: button, + checkbox: checkbox, + contextMenuAction: contextMenuAction, + emoji: emoji, + emojiButton: emojiButton, + fileTypeIcon: fileTypeIcon, + onlineIndicator: onlineIndicator, + progressBar: progressBar, + extensions: _extensionIterableToMap(extensions), + ); + } + + /// Creates component builders from a pre-built extensions map. + const StreamComponentBuilders.raw({ + required this.avatar, + required this.avatarGroup, + required this.avatarStack, + required this.badgeCount, + required this.button, + required this.checkbox, + required this.contextMenuAction, + required this.emoji, + required this.emojiButton, + required this.fileTypeIcon, + required this.onlineIndicator, + required this.progressBar, + required this.extensions, }); + /// Arbitrary additions to this builder set. + /// + /// To define extensions, pass an [Iterable] of [StreamComponentBuilderExtension] instances to + /// [StreamComponentBuilders.new]. + /// + /// To obtain an extension, use [extension]. + /// + /// See also: + /// + /// * [extension], a convenience function for obtaining a specific extension. + final Map> extensions; + + /// Used to obtain a [StreamComponentBuilder] from [extensions]. + /// + /// Obtain with `StreamComponentFactory.of(context).extension()`. + /// + /// See [extensions]. + StreamComponentBuilder? extension() => (extensions[T] as StreamComponentBuilderExtension?)?.call; + /// Custom builder for avatar widgets. /// /// When null, [StreamAvatar] uses [DefaultStreamAvatar]. @@ -223,6 +304,69 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// /// When null, [StreamProgressBar] uses [DefaultStreamProgressBar]. final StreamComponentBuilder? progressBar; + + // Convert the [extensionsIterable] passed to [StreamComponentBuilders.new] + // to the stored [extensions] map, where each entry's key consists of the extension's type. + static Map> _extensionIterableToMap( + Iterable> extensionsIterable, + ) { + return >{ + for (final extension in extensionsIterable) extension.type: extension, + }; + } +} + +/// A typed builder extension for [StreamComponentBuilders]. +/// +/// Wraps a single [StreamComponentBuilder] and uses the Props type [T] as the +/// lookup key. This allows external packages to register custom component +/// builders without modifying the core [StreamComponentBuilders] class. +/// +/// {@tool snippet} +/// +/// Register a custom message builder: +/// +/// ```dart +/// StreamComponentFactory( +/// builders: StreamComponentBuilders( +/// extensions: [ +/// StreamComponentBuilderExtension( +/// builder: (context, props) => MyCustomMessage(props: props), +/// ), +/// ], +/// ), +/// child: child, +/// ) +/// ``` +/// +/// Consume it inside a custom component: +/// +/// ```dart +/// final builder = StreamComponentFactory.of(context).extension(); +/// if (builder != null) return builder(context, props); +/// return DefaultStreamMessage(props: props); +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamComponentBuilders], which holds extensions. +/// * [StreamComponentBuilders.extension], for obtaining a specific extension. +@immutable +final class StreamComponentBuilderExtension { + /// Creates a builder extension for a component with Props type [T]. + const StreamComponentBuilderExtension({ + required StreamComponentBuilder builder, + }) : _builder = builder; + + // The internal builder function that creates the widget from the context and props. + final StreamComponentBuilder _builder; + + /// Invokes the builder with the given [context] and [props]. + Widget call(BuildContext context, T props) => _builder(context, props); + + /// The extension's type, used as the lookup key. + Object get type => T; } /// Extension on [BuildContext] for convenient access to [StreamComponentBuilders]. diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart index d9ea8d1..be9b3b3 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart @@ -29,7 +29,8 @@ mixin _$StreamComponentBuilders { return t == 0.0 ? a : null; } - return StreamComponentBuilders( + return StreamComponentBuilders.raw( + extensions: t < 0.5 ? a.extensions : b.extensions, avatar: t < 0.5 ? a.avatar : b.avatar, avatarGroup: t < 0.5 ? a.avatarGroup : b.avatarGroup, avatarStack: t < 0.5 ? a.avatarStack : b.avatarStack, @@ -46,6 +47,7 @@ mixin _$StreamComponentBuilders { } StreamComponentBuilders copyWith({ + Map>? extensions, Widget Function(BuildContext, StreamAvatarProps)? avatar, Widget Function(BuildContext, StreamAvatarGroupProps)? avatarGroup, Widget Function(BuildContext, StreamAvatarStackProps)? avatarStack, @@ -62,7 +64,8 @@ mixin _$StreamComponentBuilders { }) { final _this = (this as StreamComponentBuilders); - return StreamComponentBuilders( + return StreamComponentBuilders.raw( + extensions: extensions ?? _this.extensions, avatar: avatar ?? _this.avatar, avatarGroup: avatarGroup ?? _this.avatarGroup, avatarStack: avatarStack ?? _this.avatarStack, @@ -90,6 +93,7 @@ mixin _$StreamComponentBuilders { } return copyWith( + extensions: other.extensions, avatar: other.avatar, avatarGroup: other.avatarGroup, avatarStack: other.avatarStack, @@ -118,7 +122,8 @@ mixin _$StreamComponentBuilders { final _this = (this as StreamComponentBuilders); final _other = (other as StreamComponentBuilders); - return _other.avatar == _this.avatar && + return _other.extensions == _this.extensions && + _other.avatar == _this.avatar && _other.avatarGroup == _this.avatarGroup && _other.avatarStack == _this.avatarStack && _other.badgeCount == _this.badgeCount && @@ -138,6 +143,7 @@ mixin _$StreamComponentBuilders { return Object.hash( runtimeType, + _this.extensions, _this.avatar, _this.avatarGroup, _this.avatarStack, From 2c4c123b9b58221f85f5b8ddd8b7f3df6c20db7f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 26 Feb 2026 01:14:16 +0530 Subject: [PATCH 2/2] chore: regenerate component builder extension --- .../lib/src/factory/stream_component_factory.g.theme.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart index be9b3b3..d8f3891 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart @@ -47,7 +47,7 @@ mixin _$StreamComponentBuilders { } StreamComponentBuilders copyWith({ - Map>? extensions, + Map>? extensions, Widget Function(BuildContext, StreamAvatarProps)? avatar, Widget Function(BuildContext, StreamAvatarGroupProps)? avatarGroup, Widget Function(BuildContext, StreamAvatarStackProps)? avatarStack,