diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 5eb27f7dc2..963333f11f 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -9,6 +9,7 @@ 🐞 Fixed - Fixed `StreamChannel`'s default error state exposing raw error details; it now shows a safe error state (icon, message, and a Try Again button wired to `retry()`) that adapts to the failure type. +- Fixed `StreamChannelListController` crashing with a null-check error when its local sort ran over a channel disposed mid-query (e.g. a client disconnect/logout racing an in-flight `loadMore`); such channels are now sorted last instead. ## 10.2.0 diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index f74b721491..9e10b5e8d7 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -155,8 +155,17 @@ class StreamChannelListController extends PagedValueNotifier { orElse: () => newValue, (success) => success.copyWith( items: success.items.sortedByCompare( - (it) => it.state!.channelState, - channelSort.compare, + // A channel loses its state when it is disposed — e.g. a client + // disconnect/logout or a channel-removal event racing an + // in-flight query — so sort stateless channels last instead of + // null-asserting on them. + (it) => it.state?.channelState, + (a, b) => switch ((a, b)) { + (null, null) => 0, + (null, _) => 1, + (_, null) => -1, + (final a?, final b?) => channelSort.compare(a, b), + }, ), ), ), diff --git a/packages/stream_chat_flutter_core/test/stream_channel_list_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_list_controller_test.dart index 491c1bd0ab..685202b48a 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_list_controller_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_list_controller_test.dart @@ -275,6 +275,33 @@ void main() { expect((captured.single as PaginationParams).offset, equals(nextPageKey)); }); + test('local sort places channels with disposed state last instead of crashing', () { + ChannelState channelStateFor({required DateTime createdAt}) => ChannelState( + channel: ChannelModel(cid: 'messaging:${createdAt.millisecondsSinceEpoch}', createdAt: createdAt), + ); + + final older = MockChannel(); + when(() => older.state.channelState).thenReturn(channelStateFor(createdAt: DateTime(2026, 1, 1))); + + final newer = MockChannel(); + when(() => newer.state.channelState).thenReturn(channelStateFor(createdAt: DateTime(2026, 6, 1))); + + // A channel disposed while a query is in flight (client disconnect or + // logout, a channel-removal event) has its state nulled out. + final disposed = NonInitializedMockChannel(); + + final controller = StreamChannelListController( + client: client, + channelStateSort: defaultChannelListSort, + ); + + expect( + () => controller.value = PagedValue(items: [disposed, older, newer]), + returnsNormally, + ); + expect(controller.value.asSuccess.items, equals([newer, older, disposed])); + }); + // The controller's only responsibility for events is routing them to the // matching handler method. The handler's behavior for each event is covered // in stream_channel_list_event_handler_test.dart, so here we inject a mock