Skip to content

SfCalendar: LateInitializationError '_scrollStartPosition' when viewNavigationMode changes from none to snap during an in-flight drag #2547

Description

@oooooocean

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcalendarCalendar componentsolvedSolved the query using existing solutions

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions