diff --git a/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart b/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart index 16d3c7f..75ab76e 100644 --- a/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart +++ b/lib/state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart @@ -21,6 +21,7 @@ class LoadableStateConsumer< final Widget Function(TState state, bool loading) child; final void Function(TState state)? onLoad; final bool wrapWithScrollView; + final bool enablePullToRefresh; /// Optional predicate for callers whose [TState] always contains a non-null /// envelope but where actual content (e.g. a nested response) is loaded @@ -33,6 +34,7 @@ class LoadableStateConsumer< required this.child, this.onLoad, this.wrapWithScrollView = false, + this.enablePullToRefresh = true, this.isReady, super.key, }); @@ -62,29 +64,34 @@ class LoadableStateConsumer< final showError = hasError && !hasContent; final showErrorBar = hasError && hasContent; - var childWidget = ConditionalWrapper( - condition: loadableState.reFetch != null, - wrapper: (child) => RefreshIndicator( - onRefresh: () { - if (loadableState.reFetch != null) loadableState.reFetch!(); - return Future.value(); - }, - child: ConditionalWrapper( - condition: wrapWithScrollView, - wrapper: (child) => SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: child, - ), - child: child, - ), - ), - child: SizedBox( - height: MediaQuery.of(context).size.height, - child: hasContent - ? child(typedData as TState, isLoading) - : const SizedBox.shrink(), - ), + // Keep the wrapper hierarchy stable across refresh cycles. The bloc clears + // reFetch to null while a refetch is in flight and restores it on + // completion; flipping the RefreshIndicator in and out on that signal + // would change the widget tree under the ListView and reset its scroll + // position every refresh. + final content = SizedBox( + height: MediaQuery.of(context).size.height, + child: hasContent + ? child(typedData as TState, isLoading) + : const SizedBox.shrink(), ); + final scrollable = ConditionalWrapper( + condition: wrapWithScrollView, + wrapper: (child) => SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: child, + ), + child: content, + ); + final childWidget = enablePullToRefresh + ? RefreshIndicator( + onRefresh: () { + loadableState.reFetch?.call(); + return Future.value(); + }, + child: scrollable, + ) + : scrollable; return BlocModule( create: (context) => LoadableStateBloc(), diff --git a/lib/view/pages/talk/chat_view.dart b/lib/view/pages/talk/chat_view.dart index 900b222..0d62c39 100644 --- a/lib/view/pages/talk/chat_view.dart +++ b/lib/view/pages/talk/chat_view.dart @@ -375,6 +375,7 @@ class _ChatViewState extends State with RouteAware { isReady: (state) => state.chatResponse != null && state.currentToken == widget.room.token, + enablePullToRefresh: false, child: (state, _) { final items = _buildMessages(state.chatResponse!).reversed.toList();