Bug description
_CustomCalendarScrollViewState._scrollStartPosition (lib/src/calendar/views/calendar_view.dart) is declared late double and is only assigned in the ViewNavigationMode.snap branch of _onHorizontalStart / _onVerticalStart — the ViewNavigationMode.none branch returns without assigning. _onHorizontalUpdate / _onVerticalUpdate read the field unguarded in their snap branch.
If the host app changes SfCalendar.viewNavigationMode from none to snap while a view-swipe drag is in flight (the calendar widget rebuilds, but the internal State is preserved), the same drag's next update callback takes the snap branch and reads the never-assigned late field:
LateInitializationError: Field '_scrollStartPosition' has not been initialized.
This is a production fatal crash for us (Crashlytics, Android, syncfusion_flutter_calendar 33.2.8). The field and guard structure are unchanged on current master (34.x), so the latest version is affected as well.
Note that toggling viewNavigationMode at runtime is sanctioned usage — KB article 10950 ("How to restrict the view navigation in the Flutter Calendar", https://support.syncfusion.com/kb/article/10950) demonstrates exactly this pattern. Our app disables page navigation during a pinch-zoom interaction and re-enables it on a short timer; when the timer fires while the user is still dragging, the crash reproduces.
Steps to reproduce
Deterministic widget-test reproduction (crashes on 33.2.8 and on master):
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
void main() {
testWidgets('viewNavigationMode none→snap flip mid-drag crashes', (tester) async {
Widget host(ViewNavigationMode mode) => MaterialApp(
home: Scaffold(
body: SfCalendar(view: CalendarView.day, viewNavigationMode: mode),
),
);
await tester.pumpWidget(host(ViewNavigationMode.none));
await tester.pumpAndSettle();
// Drag starts while navigation is disabled: _onHorizontalStart's `none`
// branch returns without recording _scrollStartPosition.
final gesture = await tester.startGesture(tester.getCenter(find.byType(SfCalendar)));
await gesture.moveBy(const Offset(-40, 0));
await tester.pump();
// Host re-enables navigation mid-drag (same State — no key change).
await tester.pumpWidget(host(ViewNavigationMode.snap));
await tester.pump();
// The same in-flight drag now takes the snap branch in _onHorizontalUpdate
// and reads the uninitialized `late` field → LateInitializationError.
await gesture.moveBy(const Offset(-40, 0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
});
}
The vertical axis (CalendarView.month + MonthNavigationDirection.vertical, _onVerticalUpdate) reproduces the same way.
Expected behavior
The drag survives the mode change without crashing (e.g. treat the first post-flip update as the drag baseline).
Suggested fix
Make the field nullable and seed it on first read in the update handlers, resetting at drag end:
double? _scrollStartPosition;
// _onHorizontalUpdate (snap branch):
final double scrollStartPosition =
_scrollStartPosition ??= dragUpdateDetails.globalPosition.dx;
final double difference =
dragUpdateDetails.globalPosition.dx - scrollStartPosition;
// _onHorizontalEnd / _onVerticalEnd (entry):
_scrollStartPosition = null;
Seeding from the current pointer keeps difference == 0 on the flipped tick, so there is no crash and no spurious page turn from a zero/stale baseline. (Initializing the field to 0 instead would compute difference from the absolute screen coordinate and trigger an unintended page navigation.) Normal snap-only drags are behavior-identical since the start handler always assigns before the first update.
We are running this fix in production via a fork and are happy to provide more detail.
- Package: syncfusion_flutter_calendar 33.2.8 (also present on master / 34.x source)
- Platform: Android (mechanism is platform-independent)
- Flutter: 3.44.0
Bug description
_CustomCalendarScrollViewState._scrollStartPosition(lib/src/calendar/views/calendar_view.dart) is declaredlate doubleand is only assigned in theViewNavigationMode.snapbranch of_onHorizontalStart/_onVerticalStart— theViewNavigationMode.nonebranch returns without assigning._onHorizontalUpdate/_onVerticalUpdateread the field unguarded in theirsnapbranch.If the host app changes
SfCalendar.viewNavigationModefromnonetosnapwhile a view-swipe drag is in flight (the calendar widget rebuilds, but the internal State is preserved), the same drag's next update callback takes thesnapbranch and reads the never-assignedlatefield:This is a production fatal crash for us (Crashlytics, Android, syncfusion_flutter_calendar 33.2.8). The field and guard structure are unchanged on current master (34.x), so the latest version is affected as well.
Note that toggling
viewNavigationModeat runtime is sanctioned usage — KB article 10950 ("How to restrict the view navigation in the Flutter Calendar", https://support.syncfusion.com/kb/article/10950) demonstrates exactly this pattern. Our app disables page navigation during a pinch-zoom interaction and re-enables it on a short timer; when the timer fires while the user is still dragging, the crash reproduces.Steps to reproduce
Deterministic widget-test reproduction (crashes on 33.2.8 and on master):
The vertical axis (
CalendarView.month+MonthNavigationDirection.vertical,_onVerticalUpdate) reproduces the same way.Expected behavior
The drag survives the mode change without crashing (e.g. treat the first post-flip update as the drag baseline).
Suggested fix
Make the field nullable and seed it on first read in the update handlers, resetting at drag end:
Seeding from the current pointer keeps
difference == 0on the flipped tick, so there is no crash and no spurious page turn from a zero/stale baseline. (Initializing the field to0instead would computedifferencefrom the absolute screen coordinate and trigger an unintended page navigation.) Normal snap-only drags are behavior-identical since the start handler always assigns before the first update.We are running this fix in production via a fork and are happy to provide more detail.