Detecting unsaved changes in PDFs
Call hasUnsavedChanges() on a document to check whether it has unsaved changes. Use this check before you close a document or navigate away. Refer to the has unsaved changes(opens in a new tab) API reference for details.
Detect changes across platforms
The hasUnsavedChanges() method works on all platforms. You can call it on the viewer’s controller.document or on a document you opened headlessly with Nutrient.openDocument():
final hasChanges = await controller.document.hasUnsavedChanges();
if (hasChanges) { // Prompt the user to save, or save directly. await controller.document.save();}Each platform checks a different set of document changes:
| Platform | Checks |
|---|---|
| Android | Annotations, forms, bookmarks |
| iOS | Annotations |
| Web | Annotations, forms, bookmarks, comments |
Track changes with events
Use the controller’s typed event stream to keep a reactive dirty-state flag in sync with the document. This works well when you need to enable a Save button or control PopScope. Mark the document as dirty when an annotation or form event fires. Clear the flag when DocumentSavedEvent confirms the save. Refer to the document saved event(opens in a new tab) API reference for details.
StreamSubscription<NutrientEvent>? _eventsSub;bool _hasUnsavedChanges = false;
void _onControllerReady(NutrientController controller) { _controller = controller; _eventsSub = controller.events.listen((event) { switch (event) { // Any annotation or form mutation means the document diverged from disk. case AnnotationCreatedEvent(): case AnnotationUpdatedEvent(): case AnnotationDeletedEvent(): case FormFieldUpdatedEvent(): setState(() => _hasUnsavedChanges = true); // The platform confirmed the write landed — the document now matches // disk again. case DocumentSavedEvent(): setState(() => _hasUnsavedChanges = false); default: break; } });}Cancel the subscription in dispose().
Confirm before closing
The following example prompts the user before closing a document with unsaved changes. It uses PopScope, which replaces WillPopScope in Flutter 3.12 and later:
import 'dart:async';
import 'package:flutter/material.dart';import 'package:nutrient_flutter/bindings.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> { NutrientController? _controller; StreamSubscription<NutrientEvent>? _eventsSub; bool _hasUnsavedChanges = false;
void _onControllerReady(NutrientController controller) { _controller = controller; _eventsSub = controller.events.listen((event) { switch (event) { case AnnotationCreatedEvent(): case AnnotationUpdatedEvent(): case AnnotationDeletedEvent(): case FormFieldUpdatedEvent(): setState(() => _hasUnsavedChanges = true); case DocumentSavedEvent(): setState(() => _hasUnsavedChanges = false); default: break; } }); }
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 _controller?.document.save(); }
if (context.mounted) { Navigator.pop(context); } }
@override void dispose() { _eventsSub?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { return PopScope( canPop: !_hasUnsavedChanges, onPopInvokedWithResult: (didPop, result) => _handlePopInvoked(didPop), child: Scaffold( appBar: AppBar(title: const Text('Document')), body: NutrientDocumentView( documentPath: widget.documentPath, onControllerReady: _onControllerReady, ), ), ); }}Keep these points in mind:
- The
canPopproperty determines whether the route enables back navigation. Set it tofalsewhen the document has unsaved changes. - The
onPopInvokedWithResultcallback runs when the user attempts to pop the route. IfdidPopisfalse, Flutter blocked navigation and you can show a confirmation dialog. - The event stream keeps
_hasUnsavedChangesup to date. Asave()result oftrueand theDocumentSavedEventclear the flag.
For a complete runnable example, open manual_save_example in the catalog app.
The granular platform-specific dirty-state methods from the legacy method-channel PdfDocument API (iOSGetAnnotationIsDirty(), iOSClearNeedsSaveFlag(), androidGetBookmarkIsDirty(), and so on) aren’t available on the SDK 6 document. Use the cross-platform hasUnsavedChanges() method or event-based tracking, or access the native dirty-state APIs directly. Refer to the platform adapters guide and the migration guide for details.