---
title: "Detect unsaved changes in PDFs | Nutrient Flutter SDK"
canonical_url: "https://www.nutrient.io/guides/flutter/save-a-document/detect-unsaved-changes/"
md_url: "https://www.nutrient.io/guides/flutter/save-a-document/detect-unsaved-changes.md"
last_updated: "2026-07-03T00:00:00.000Z"
description: "Learn how to detect unsaved changes in PDF documents using Nutrient Flutter SDK with `hasUnsavedChanges()` and event-based dirty tracking across all platforms."
---

# 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](https://pub.dev/documentation/nutrient_flutter/latest/nutrient_flutter.bindings/NutrientDocumentInterface/hasUnsavedChanges.html) 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()`:

```dart

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](https://pub.dev/documentation/nutrient_flutter/latest/nutrient_flutter.bindings/DocumentSavedEvent-class.html) API reference for details.

```dart

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:

```dart

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](https://www.nutrient.io/guides/flutter/platform-adapters.md) guide and the [migration guide](https://www.nutrient.io/guides/flutter/migration-guides/flutter-6-migration-guide.md) for details.
---

## Related pages

- [Auto save PDF files in Flutter](/guides/flutter/save-a-document.md)
- [Conflict resolution](/guides/flutter/save-a-document/conflict-resolution.md)
- [Use Save as for PDFs in Flutter](/guides/flutter/save-a-document/save-as.md)
- [Supported document save options in Flutter](/guides/flutter/save-a-document/save-options.md)
- [Save a document to a remote server in Flutter](/guides/flutter/save-a-document/save-to-remote.md)

