show<T> static method

Future<CNAlertResult<T>?> show<T>({
  1. required BuildContext context,
  2. required List<CNAlertAction<T>> actions,
  3. String? title,
  4. String? message,
  5. List<CNAlertTextField> textFields = const <CNAlertTextField>[],
})

Presents the alert and resolves to what the user answered.

Resolves to null when no answer was produced: the plugin is not registered, the native presentation failed, the fallback route was popped without a button press, or — on iOS only — a second CNAlert.show superseded this one, taking its alert down and resolving this call to null. The Flutter fallback stacks instead: the earlier dialog stays up and resolves on its own button press. A pressed button — including a cancel button — always resolves to a CNAlertResult.

title and message are the alert's header. textFields adds one input row each, in order, and their contents come back in CNAlertResult.textFields.

Apple's Human Interface Guidelines recommend at most two buttons for an alert; more work, but stack vertically.

Asserts in debug, and returns null without presenting anything in release, when actions is empty or every action is disabled — a .alert cannot be dismissed by tapping outside, so every way out of it runs an action handler. An alert with no button, or none that accepts a tap, can never be dismissed and the returned future would never complete.

Also asserts against configurations UIKit cannot render: two actions with CNAlertAction.isCancel (a hard NSInternalInconsistencyException), two with CNAlertAction.isDefault, and a single action that is both cancel and destructive.

Implementation

static Future<CNAlertResult<T>?> show<T>({
  required BuildContext context,
  required List<CNAlertAction<T>> actions,
  String? title,
  String? message,
  List<CNAlertTextField> textFields = const <CNAlertTextField>[],
}) {
  assert(
    actions.isNotEmpty,
    'CNAlert.show needs at least one action: an alert cannot be dismissed by '
    'tapping outside, so one with no buttons never resolves.',
  );
  assert(
    actions.any((a) => a.enabled),
    'CNAlert.show needs at least one enabled action: an alert cannot be '
    'dismissed by tapping outside, so one whose buttons all refuse taps '
    'never resolves.',
  );
  assert(
    actions.where((a) => a.isCancel).length <= 1,
    'CNAlert.show accepts at most one action with isCancel: UIKit raises '
    'NSInternalInconsistencyException on a second .cancel action.',
  );
  assert(
    actions.where((a) => a.isDefault).length <= 1,
    'CNAlert.show accepts at most one action with isDefault: '
    'UIAlertController.preferredAction holds a single action.',
  );
  assert(
    !actions.any((a) => a.isCancel && a.isDestructive),
    'A CNAlertAction cannot be both isCancel and isDestructive: '
    'UIAlertAction.Style is one value, so one of the two would be dropped.',
  );

  // `actions` is often built from a collection that can come out empty, and
  // `enabled` is often computed per action from one flag that can be false for
  // all of them. The native alert would then have no button that answers a
  // tap and no dismissal path, so nothing could complete the call and the
  // caller's await would hang for the life of the process. Answer it here
  // instead of presenting a modal there is no way out of.
  if (!actions.any((a) => a.enabled)) {
    return Future<CNAlertResult<T>?>.value(null);
  }

  if (defaultTargetPlatform != TargetPlatform.iOS) {
    return _showFallback<T>(
      context: context,
      actions: actions,
      title: title,
      message: message,
      textFields: textFields,
    );
  }

  return _showNative<T>(
    actions: actions,
    title: title,
    message: message,
    textFields: textFields,
  );
}