dismissDialog static method
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();
});
}