dismissDialog static method

Future<void> dismissDialog({
  1. String? id,
  2. VoidCallback? onDismissed,
})

Dismisses only the dialog by its ID

Parameters:

  • id: The ID of the dialog to dismiss (required)
  • onDismissed: Optional callback executed after the dialog is dismissed

Example:

// Dismiss current dialog (less safe)
Modal.dismissDialog();

// Dismiss specific dialog by ID (recommended)
Modal.dismissDialog(id: 'settings_dialog');

Implementation

static Future<void> dismissDialog(
    {String? id, VoidCallback? onDismissed}) async {
  _debugModalLog(
    'dismissDialog id=$id isDialogActive=${Modal.isDialogActive} isDismissing=${Modal.isDialogDismissing}',
  );
  final dialogs = _dialogStackNotifier.state;
  if (dialogs.isEmpty) return;

  final targetIndex = id == null
      ? dialogs.length - 1
      : dialogs.lastIndexWhere(
          (dialog) => dialog.id == id || dialog.uniqueId == id,
        );
  if (targetIndex < 0) return;

  final targetDialog = dialogs[targetIndex];
  final isTopMost = targetIndex == dialogs.length - 1;

  if (!isTopMost) {
    final updatedStack = List<_ModalContent>.from(dialogs)
      ..removeAt(targetIndex);
    _dialogStackNotifier.state = updatedStack;
    _syncDialogControllersFromStack();
    _syncInterleavedDialogLayersWithStack();

    _unregisterModal(targetDialog.uniqueId);
    _dispatchModalLifecycleEvent(_buildModalLifecycleEvent(
        targetDialog, ModalLifecycleEventType.dismissed));
    onDismissed?.call();
    targetDialog.onDismissed?.call();
    return;
  }

  if (Modal.isDialogDismissing) return;

  final targetDialogId = targetDialog.uniqueId;
  final remainingDialogsAfterDismiss = dialogs
      .where((dialog) => dialog.uniqueId != targetDialogId)
      .toList(growable: false);
  final hasRemainingDialogs = remainingDialogsAfterDismiss.isNotEmpty;
  final remainingDialogNeedsBlur = remainingDialogsAfterDismiss
      .any((dialog) => dialog.shouldBlurBackground);

  Modal.isDismissing = true;
  _dialogDismissingNotifier.state = true;

  // Capture the modal type that is being dismissed NOW so we can clear
  // the correct type-specific controller after callbacks run.
  final originallyDismissedType = _activeModalController.state?.modalType;

  // Capture the modal type being dismissed so we clear the correct
  // type-specific controller even if callbacks change the active modal.
  // Note: We intentionally do not capture dismissed type here because
  // bottom sheet cleanup is handled below when we refresh the controller.

  // Reset blur animation state - BUT ONLY if no other blur-enabled modal remains active
  // If a bottom sheet OR remaining dialog with blur is still showing, preserve blur.
  final sheetNeedsBlur = Modal.isSheetActive &&
      (_sheetController.state?.shouldBlurBackground ?? false);
  if (!sheetNeedsBlur && !remainingDialogNeedsBlur) {
    _blurAnimationStateNotifier.state = 0.0;
    _blurAmountNotifier.state = 0.0;
  }

  // Animate background out only when no dialog remains and no sheet is active.
  if (!Modal.isSheetActive && !hasRemainingDialogs) {
    _backgroundAnimationTimer?.cancel();
    // Decreased duration for faster dismissal feedback
    const animSteps = 10;
    const totalDuration = 200;
    final stepDuration = Duration(milliseconds: totalDuration ~/ animSteps);
    double startValue = _backgroundLayerAnimationNotifier.state;
    double step = startValue / animSteps;
    int currentStep = 0;

    _backgroundAnimationTimer = Timer.periodic(stepDuration, (timer) {
      currentStep++;
      if (currentStep > animSteps) {
        timer.cancel();
        _backgroundAnimationTimer = null;
        _backgroundLayerAnimationNotifier.state = 0.0;
      } else {
        _backgroundLayerAnimationNotifier.state =
            startValue - (step * currentStep);
      }
    });
  } else if (hasRemainingDialogs) {
    // Keep barrier visuals fully visible for the remaining dialog stack.
    _backgroundAnimationTimer?.cancel();
    _backgroundAnimationTimer = null;
    _backgroundLayerAnimationNotifier.state =
        max(_backgroundLayerAnimationNotifier.state, 1.0);
  }

  // Animate out
  _dismissModalAnimationController.state = true;

  // Allow animation time, then perform cleanup and run callbacks AFTER
  // the modal is effectively torn down. This avoids race conditions
  // where callbacks show new snackbars and the cleanup later clears them.
  // debugPrint(
  //     'Modal.dismissDialog: start (activeId=${_activeModalController.state?.uniqueId}, dialogId=${_dialogController.state?.uniqueId})');
  await Future.delayed(0.2.sec, () {
    // VALIDATE: Check if the dialog ID still matches what we intended to dismiss
    final currentDialogId = _dialogController.state?.uniqueId;
    if (currentDialogId != null && currentDialogId != targetDialogId) {
      // debugPrint(
      //     'Modal.dismissDialog: WARNING - Dialog ID changed during animation! '
      //     'Target=$targetDialogId, Current=$currentDialogId. Aborting cleanup.');
      Modal.isDismissing = false;
      _dialogDismissingNotifier.state = false;
      return;
    }

    // debugPrint(
    //     'Modal.dismissDialog: animation complete, running cleanup for ID=$targetDialogId');
    // debugPrint(
    //     'Modal.dismissDialog: before cleanup: active=${Modal.controller.state?.modalType} activeId=${Modal.controller.state?.uniqueId} dialogId=${_dialogController.state?.uniqueId} snackbarQueue=${_snackbarQueueNotifier.state.length}');

    // Capture the dialog content and callback before removing it from the stack.
    final dismissedDialog = targetDialog;
    final dialogOnDismiss = targetDialog.onDismissed;

    _removeDialogFromStack(targetDialogId);

    // Unregister from modal registry
    final updatedRegistry = Map<String, ModalType>.from(_modalRegistry.state);
    updatedRegistry.remove(targetDialogId);
    _modalRegistry.state = updatedRegistry;

    // Run callbacks now that the dialog has been cleaned up
    // debugPrint('Modal.dismissDialog: running callbacks (post-refresh)');
    _dispatchModalLifecycleEvent(_buildModalLifecycleEvent(
        dismissedDialog, ModalLifecycleEventType.dismissed));
    onDismissed?.call();
    dialogOnDismiss?.call();

    // debugPrint(
    //     'Modal.dismissDialog: after callbacks: active=${Modal.controller.state?.modalType} activeId=${Modal.controller.state?.uniqueId} dialogId=${_dialogController.state?.uniqueId} snackbarQueue=${_snackbarQueueNotifier.state.length}');

    final currentQueue = _snackbarQueueNotifier.state;
    // debugPrint(
    //     'Modal.dismissDialog: snackbar queue has ${currentQueue.keys.length} positions');

    // IMPORTANT: Do NOT clear the snackbar queue when dismissing a dialog.
    // Snackbars are independent modals and should remain visible.
    // Only update the active modal controller to point to a remaining snackbar if any.
    if (currentQueue.isNotEmpty) {
      // debugPrint(
      //     'Modal.dismissDialog: snackbar queue not empty, preserving snackbars');
      // Find the first position with snackbars and make it the active modal
      Alignment? positionWithContent;
      for (final position in currentQueue.keys) {
        if (currentQueue[position]!.isNotEmpty) {
          positionWithContent = position;
          break;
        }
      }
      if (positionWithContent != null) {
        final snackbarToShow = currentQueue[positionWithContent]!.first;
        if (_snackbarController.state?.uniqueId != snackbarToShow.uniqueId) {
          _snackbarController.state = snackbarToShow;
        }
        if (Modal.controller.state?.uniqueId != snackbarToShow.uniqueId) {
          Modal.controller.state = snackbarToShow;
        }
        // Ensure the newly activated snackbar is not marked as dismissing
        _setSnackbarDismissing(snackbarToShow.uniqueId, false);
        Modal.dismissModalAnimationController.state = false;
      }
    }

    // Clear type-specific controllers based on what was dismissed
    // IMPORTANT: Only clear active modal controller if NO other modals are active
    if (currentQueue.isEmpty &&
        !Modal.isSheetActive &&
        !Modal.isDialogActive &&
        !Modal.isCustomActive) {
      _snackbarController.refresh();
      _activeModalController.refresh();
    } else if (currentQueue.isEmpty && Modal.isDialogActive) {
      _snackbarController.refresh();
      Modal.controller.state = _dialogController.state;
    } else if (currentQueue.isEmpty && Modal.isSheetActive) {
      // Bottom sheet is still active - make it the active modal
      _snackbarController.refresh();
      Modal.controller.state = _sheetController.state;
    } else if (currentQueue.isEmpty && Modal.isCustomActive) {
      // Custom modal is still active under the dismissed dialog
      _snackbarController.refresh();
      Modal.controller.state = _customController.state;
    }
    if (originallyDismissedType == ModalType.dialog) {
      // Dialog was already refreshed above; just clear the dismissing flag
      _dialogDismissingNotifier.state = false;
    }

    // Reset dismiss animation controller if no other modals active
    if (!Modal.isSheetActive &&
        !Modal.isSnackbarActive &&
        !Modal.isDialogActive &&
        !Modal.isCustomActive) {
      _dismissModalAnimationController.state = false;
    }

    // debugPrint('Modal.dismissDialog: finished');
    if (Modal.isCustomActive) {
      _customBarrierTapSuppressedUntil =
          DateTime.now().add(const Duration(milliseconds: 250));
    }
    Modal.isDismissing = false;
    final modalLatest = Modal.latestActivationOrder;
    final popLatest = PopOverlay.latestActivationOrder;
    if (!OverlayInterleaveManager.enabled && popLatest > modalLatest) {
      PopOverlay.bringOverlayHostToFront();
    } else if (!OverlayInterleaveManager.enabled && modalLatest > popLatest) {
      Modal.bringOverlayHostToFront();
    }
    HapticFeedback.lightImpact();
    _ensureCleanStateIfIdle();
  });
}