Skip to content

[syncfusion_flutter_pdfviewer] Fatal "Null check operator used on a null value" in SfPdfViewerState._checkVisiblePages — unguarded _pdfPages[pageNumber]! race with the delayed tile-render timer #2553

Description

@Senzaiken

Bug description

SfPdfViewer crashes in release builds with a fatal TypeError: Null check operator used on a null value inside SfPdfViewerState._checkVisiblePages, leaving the user staring at a blank white viewer. The crash originates from the unguarded null-assert lookups on the internal page-layout map:

// pdfviewer.dart (33.2.12 lines ~4103-4111; byte-identical in 34.1.33)
final Rect pageRect = Rect.fromLTWH(
  _scrollDirection == PdfScrollDirection.vertical
      ? 0
      : _pdfPages[pageNumber]!.pageOffset,
  _scrollDirection == PdfScrollDirection.vertical
      ? _pdfPages[pageNumber]!.pageOffset   // <-- crash site (line 4108)
      : 0,
  _pdfPages[pageNumber]!.pageSize.width,
  _pdfPages[pageNumber]!.pageSize.height,
);

Why it happens (race): _pdfPages is repopulated during widget builds and cleared on document reset/swap, while _getTileImage schedules a delayed Timer(Durations.medium4) whose callback calls _checkVisiblePages. That callback iterates for (pageNumber = _pdfViewerController.pageNumber; pageNumber <= _pdfViewerController.pageCount; pageNumber++) and null-asserts _pdfPages[pageNumber]!. If the timer fires in a window where the map is non-empty but not (yet) complete for every page ≤ pageCount — e.g. around a rebuild/reload boundary — the ! throws. The if (_pdfPages.isEmpty) return; guard at the top of _checkVisiblePages only protects against the fully-cleared state, not the partially-populated one.

The crash is strongly amplified by large, image-heavy documents: in our fleet it fires almost exclusively on ~4.3 MB, 28-page PDFs where nearly every page carries a full-page background image (generated by TCPDF/FPDI). Slower build/layout/render passes make the delayed tile timer far more likely to land inside a bad window. It reproduces on high-end devices (Galaxy S24/S25 Ultra), so it is a timing race, not a low-memory/low-CPU issue.

Because the exception escapes through the timer callback to PlatformDispatcher.onError, onDocumentLoadFailed never fires and the app has no way to recover the viewer — end users just see a blank screen. Our crash reporter has 182 occurrences across 20 users for this exact signature (first seen Oct 2024, still occurring on 33.2.12).

Possibly related (same method, different line): #2552 reports extractText() crashing inside the same _checkVisiblePages loop; #1816 reported the sibling _getTileImage/_getSpecificTile null-assert in 2024 but was closed for lack of information.

Suggested fix: treat a missing _pdfPages[pageNumber] entry as "layout not ready yet" and skip it — the next tile-timer tick re-runs _checkVisiblePages anyway:

final PdfPageInfo? pageInfo = _pdfPages[pageNumber];
if (pageInfo == null) {
  continue;
}

(and equivalently for the other _pdfPages[...]! sites in _checkVisiblePages / the single-page-layout variant).

Steps to reproduce

We do not have a minimal deterministic repro — it is a timing race. Conditions under which we observe it in production:

  1. Load a large, image-heavy PDF (ours: 28 pages, ~4.3 MB, full-page raster background on nearly every page) via SfPdfViewer.network inside a Scaffold (form-filling screen, canShowSignaturePadDialog: false).
  2. Open the document and interact normally (scroll/zoom) around load/rebuild boundaries.
  3. Intermittently, the delayed tile-render timer fires while the page-layout map is incomplete → fatal TypeError, blank viewer. Reopening the screen usually works on a later attempt.

We can share a sample document with equivalent structure privately through a support ticket if needed.

Code sample

Code sample
SfPdfViewer.network(
  presignedUrl, // ~4.3 MB, 28-page PDF, full-page images on most pages
  controller: _controller,
  canShowSignaturePadDialog: false,
  onDocumentLoaded: (_) => _onDocumentLoaded(),
  onFormFieldFocusChange: _onFieldFocusChange,
  onFormFieldValueChanged: _onTextChanged,
)

Screenshots or Video

Not applicable — the user-visible symptom is a blank white viewer body after the fatal error.

Stack Traces

Stack Traces

Captured by Sentry from a release (AOT) build, mechanism: PlatformDispatcher.onError, level: fatal:

TypeError: Null check operator used on a null value
    at SfPdfViewerState._checkVisiblePages (pdfviewer.dart:4108)
    at SfPdfViewerState._getTileImage (pdfviewer.dart:4621)
    at new Future.delayed.<fn> (future.dart:440)
    at TickerFuture.whenCompleteOrCancel.thunk (ticker.dart:464)
    at _Timer._runTimers (timer_impl.dart:423)
    at _Timer._handleMessage (timer_impl.dart:454)
    at _RawReceivePort._handleMessage (isolate_patch.dart:192)

Line 4108 of syncfusion_flutter_pdfviewer-33.2.12/lib/src/pdfviewer.dart is the ? _pdfPages[pageNumber]!.pageOffset expression quoted above; line 4621 is the _checkVisiblePages() call inside _getTileImage.

Recent occurrences observed on: SM-S928U (Android 16), SM-S938U, SM-S916U, SM-S906U1, SM-S711U, SM-F731U, SM-A176U — all Android 15/16, Flutter 3.44.4/3.44.6 stable, Dart 3.12.2, package 33.2.12.

On which target platforms have you observed this bug?

Android (release/AOT builds; observed on Android 15 and 16)

Flutter Doctor output

Doctor output
Flutter 3.44.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision ee80f08bbf
Dart 3.12.2 • DevTools 2.54.1
syncfusion_flutter_pdfviewer: 33.2.12 (crash site verified byte-identical in 34.1.33)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions