This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/flutter/save-a-document/detect-unsaved-changes.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Detect unsaved changes in PDFs | Nutrient Flutter SDK

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:

PlatformChecks
AndroidAnnotations, forms, bookmarks
iOSAnnotations
WebAnnotations, 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 canPop property determines whether the route enables back navigation. Set it to false when the document has unsaved changes.
  • The onPopInvokedWithResult callback runs when the user attempts to pop the route. If didPop is false, Flutter blocked navigation and you can show a confirmation dialog.
  • The event stream keeps _hasUnsavedChanges up to date. A save() result of true and the DocumentSavedEvent clear 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.