I have a Flutter app that uses a NestedScrollView with an IndexedStack as its body. Each screen within the IndexedStack is either a form or a custom widget. My goal is to scroll to the first invalid form field when validation fails. For this, I am using Scrollable.ensureVisible to bring the invalid field into view. However, in my current implementation, the scroll behavior is inconsistent:
- The invalid field does not always scroll into view.
- If it does scroll, the field is not positioned in the middle of the visible screen area as I intended.
This issue is especially problematic when switching between steps in the IndexedStack.
Here’s what I tried:
I validated the form and identified the first invalid field using its GlobalKey.
Used Scrollable.ensureVisible on the context of the first invalid field.
Experimented with different alignment values (e.g., 0.0, 0.5, 1.0) and added a Duration with easing curves to smooth the scroll.
Expected Behavior:
The app should scroll to the specific invalid field, and the field should be centered on the screen.
Actual Results:
The field does not always scroll into view.
If it scrolls, it is not reliably centered in the visible portion of the screen.
@override
Widget build(BuildContext context) {
return BaseAppScreenBg(
child: Scaffold(
appBar: AppBarWidget(title: ''),
body: BlocBuilder<CustomStepperBloc, CustomStepperState>(
buildWhen: (previous, current) =>
current.status == CustomStepperEnum.step ||
current.status == CustomStepperEnum.submitStep ||
current.status == CustomStepperEnum.showProgress ||
current.status == CustomStepperEnum.hideProgress,
builder: (context, state) {
if (state.isLoading) {
return Center(
child: BaseCircularProgressWidget(visible: true),
);
}
var children = state.totalSteps == 3
? [
StepOneWidget(
key: stepOneKey,
customStepperBloc: _customStepperBloc,
controller: _scrollController,
),
StepTwoWidget(
key: stepTwoKey,
customStepperBloc: _customStepperBloc,
controller: _scrollController,
),
DynamicFormScreen(key: stepThreeKey),
]
: [
StepOneWidget(
key: stepOneKey,
customStepperBloc: _customStepperBloc,
controller: _scrollController,
),
StepTwoWidget(
key: stepTwoKey,
customStepperBloc: _customStepperBloc,
controller: _scrollController,
),
];
return Stack(
children: [
NestedScrollView(
controller: _customStepperBloc.parentController,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: buildBannerWidget(),
),
buildStepperWidget(state),
];
},
body: IndexedStack(
clipBehavior: Clip.hardEdge,
index: state.currentStep,
children: children,
),
),
_buildProgressWidget(),
],
);
},
),
),
);
}
void _handleInvalidForm(List<List<dynamic>> forms, int invalidFormIndex, Emitter<CustomStepperState> emit) {
final fields = forms[invalidFormIndex][1] as List<GlobalKey<FormFieldState>>;
final step = forms[invalidFormIndex][2] as int;
emit(state.copyWith(status: CustomStepperEnum.submitStep, currentStep: step, time: TimeUtils.getTimeMili()));
final targetContext = fields
.firstWhere(
(key) => key.toString() == getFirstErrorField(fields),
orElse: () => GlobalKey<FormFieldState>(),
)
.currentContext;
if (targetContext != null) {
Scrollable.ensureVisible(
targetContext,
alignment: 0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
}
Widget stepOneWidget(BuildContext mainContext) {
return Builder(builder: (context) {
var absorberHandle = NestedScrollView.sliverOverlapAbsorberHandleFor(context);
AppLog.i('Form Field One Load');
return CustomScrollView(
primary: true, // Use the primary scroll controller
physics: const ClampingScrollPhysics(), // Use consistent scroll
// controller: PrimaryScrollController.of(context),
slivers: [
SliverOverlapInjector(handle: absorberHandle),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: smallPadding),
child: buildFormWidget(context),
),
)
],
);
});
}
Kaushik bhut is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.