Detecting unsaved changes in PDFs
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:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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:
canPopdetermines whether back navigation is allowed (set tofalsewhen there are unsaved changes)onPopInvokedWithResultis called when a pop is attempted — ifdidPopisfalse, the navigation was blocked and you can show a confirmation dialog- You need to track changes proactively and update the
_hasUnsavedChangesstate