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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions