Check if a document has any unsaved changes by calling hasUnsavedChanges() on a PdfDocument instance. This is useful for prompting users to save before closing a document or navigating away.

Cross-platform detection

The hasUnsavedChanges() method works on all platforms (iOS, Android, and web):

void checkForChanges(PdfDocument document) async {
final hasChanges = await document.hasUnsavedChanges();
if (hasChanges) {
// Prompt user to save or auto-save.
await document.save();
}
}

Platform-specific methods

Each platform provides additional methods for more granular dirty state tracking.

iOS

On iOS, check and manipulate the dirty state of individual annotations:

import 'dart:io';
void checkiOSDirtyState(PdfDocument document) async {
if (!Platform.isIOS) return;
// Check if document has any dirty annotations.
final hasDirtyAnnotations = await document.iOSHasDirtyAnnotations();
if (hasDirtyAnnotations) {
// Check if a specific annotation is dirty.
final isDirty = await document.iOSGetAnnotationIsDirty(
0, // Page index.
'annotation-uuid', // Annotation ID.
);
// Clear dirty state for a specific annotation.
await document.iOSSetAnnotationIsDirty(0, 'annotation-uuid', false);
// Or clear all dirty flags at once.
await document.iOSClearNeedsSaveFlag();
}
}

The following iOS methods are available:

MethodDescription
iOSHasDirtyAnnotations()Returns true if any annotation has unsaved changes.
iOSGetAnnotationIsDirty(pageIndex, annotationId)Returns the dirty state of a specific annotation.
iOSSetAnnotationIsDirty(pageIndex, annotationId, isDirty)Sets the dirty state of a specific annotation.
iOSClearNeedsSaveFlag()Clears the needs-save flag on all annotations.

Android

On Android, check dirty state separately for annotations, forms, and bookmarks:

import 'dart:io';
void checkAndroidDirtyState(PdfDocument document) async {
if (!Platform.isAndroid) return;
// Check each provider separately.
final hasAnnotationChanges =
await document.androidHasUnsavedAnnotationChanges();
final hasFormChanges =
await document.androidHasUnsavedFormChanges();
final hasBookmarkChanges =
await document.androidHasUnsavedBookmarkChanges();
print('Unsaved changes:');
print(' Annotations: $hasAnnotationChanges');
print(' Forms: $hasFormChanges');
print(' Bookmarks: $hasBookmarkChanges');
// Check if a specific bookmark is dirty.
final bookmarkDirty =
await document.androidGetBookmarkIsDirty('bookmark-id');
// Clear dirty state for a specific bookmark.
await document.androidClearBookmarkDirtyState('bookmark-id');
// Check if a specific form field is dirty.
final fieldDirty =
await document.androidGetFormFieldIsDirty('form.field.name');
}

The following Android methods are available:

MethodDescription
androidHasUnsavedAnnotationChanges()Returns true if the annotation provider has unsaved changes.
androidHasUnsavedFormChanges()Returns true if the form provider has unsaved changes.
androidHasUnsavedBookmarkChanges()Returns true if the bookmark provider has unsaved changes.
androidGetBookmarkIsDirty(bookmarkId)Returns the dirty state of a specific bookmark.
androidClearBookmarkDirtyState(bookmarkId)Clears the dirty state of a specific bookmark.
androidGetFormFieldIsDirty(fullyQualifiedName)Returns the dirty state of a specific form field.

Web

On web, use the webHasUnsavedChanges() method:

import 'package:flutter/foundation.dart';
void checkWebDirtyState(PdfDocument document) async {
if (!kIsWeb) return;
final hasChanges = await document.webHasUnsavedChanges();
if (hasChanges) {
await document.save();
}
}

Example: Confirm before closing

The following example prompts the user before closing a document with unsaved changes. This example uses PopScope, which replaces WillPopScope (deprecated in Flutter 3.12):

import 'package:flutter/material.dart';
import 'package:nutrient_flutter/nutrient_flutter.dart';
class DocumentViewer extends StatefulWidget {
final String documentPath;
const DocumentViewer({super.key, required this.documentPath});
@override
State<DocumentViewer> createState() => _DocumentViewerState();
}
class _DocumentViewerState extends State<DocumentViewer> {
PdfDocument? _document;
bool _hasUnsavedChanges = false;
Future<void> _checkForChanges() async {
if (_document == null) return;
final hasChanges = await _document!.hasUnsavedChanges();
setState(() {
_hasUnsavedChanges = hasChanges;
});
}
Future<void> _handlePopInvoked(bool didPop) async {
if (didPop) return;
final shouldSave = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Unsaved Changes'),
content: const Text(
'You have unsaved changes. Do you want to save before leaving?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Discard'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Save'),
),
],
),
);
if (shouldSave == true) {
await _document!.save();
}
if (context.mounted) {
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !_hasUnsavedChanges,
onPopInvokedWithResult: (didPop, result) => _handlePopInvoked(didPop),
child: Scaffold(
appBar: AppBar(title: const Text('Document')),
body: NutrientView(
documentPath: widget.documentPath,
onDocumentLoaded: (document) {
_document = document;
},
onDocumentSaved: (_) => _checkForChanges(),
),
),
);
}
}

The key differences from the older WillPopScope approach:

  • canPop determines whether back navigation is allowed (set to false when there are unsaved changes)
  • onPopInvokedWithResult is called when a pop is attempted — if didPop is false, the navigation was blocked and you can show a confirmation dialog
  • You need to track changes proactively and update the _hasUnsavedChanges state