stabilized LoadableStateConsumer widget hierarchy to prevent scroll resets, added pull-to-refresh configuration, and disabled it in chat view

This commit is contained in:
2026-05-13 18:22:25 +02:00
parent 58fb843f3d
commit 6c7d217463
2 changed files with 30 additions and 22 deletions
@@ -21,6 +21,7 @@ class LoadableStateConsumer<
final Widget Function(TState state, bool loading) child; final Widget Function(TState state, bool loading) child;
final void Function(TState state)? onLoad; final void Function(TState state)? onLoad;
final bool wrapWithScrollView; final bool wrapWithScrollView;
final bool enablePullToRefresh;
/// Optional predicate for callers whose [TState] always contains a non-null /// Optional predicate for callers whose [TState] always contains a non-null
/// envelope but where actual content (e.g. a nested response) is loaded /// envelope but where actual content (e.g. a nested response) is loaded
@@ -33,6 +34,7 @@ class LoadableStateConsumer<
required this.child, required this.child,
this.onLoad, this.onLoad,
this.wrapWithScrollView = false, this.wrapWithScrollView = false,
this.enablePullToRefresh = true,
this.isReady, this.isReady,
super.key, super.key,
}); });
@@ -62,29 +64,34 @@ class LoadableStateConsumer<
final showError = hasError && !hasContent; final showError = hasError && !hasContent;
final showErrorBar = hasError && hasContent; final showErrorBar = hasError && hasContent;
var childWidget = ConditionalWrapper( // Keep the wrapper hierarchy stable across refresh cycles. The bloc clears
condition: loadableState.reFetch != null, // reFetch to null while a refetch is in flight and restores it on
wrapper: (child) => RefreshIndicator( // completion; flipping the RefreshIndicator in and out on that signal
onRefresh: () { // would change the widget tree under the ListView and reset its scroll
if (loadableState.reFetch != null) loadableState.reFetch!(); // position every refresh.
return Future.value(); final content = SizedBox(
}, height: MediaQuery.of(context).size.height,
child: ConditionalWrapper( child: hasContent
condition: wrapWithScrollView, ? child(typedData as TState, isLoading)
wrapper: (child) => SingleChildScrollView( : const SizedBox.shrink(),
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(),
),
); );
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<LoadableStateBloc, LoadableStateState>( return BlocModule<LoadableStateBloc, LoadableStateState>(
create: (context) => LoadableStateBloc(), create: (context) => LoadableStateBloc(),
+1
View File
@@ -375,6 +375,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
isReady: (state) => isReady: (state) =>
state.chatResponse != null && state.chatResponse != null &&
state.currentToken == widget.room.token, state.currentToken == widget.room.token,
enablePullToRefresh: false,
child: (state, _) { child: (state, _) {
final items = final items =
_buildMessages(state.chatResponse!).reversed.toList(); _buildMessages(state.chatResponse!).reversed.toList();