Unverified Commit 2eeeba9c authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Remove nullOk from Actions.invoke, add Actions.maybeInvoke (#74680)

This removes the nullOk parameter from Actions.invoke, and adds an Actions.maybeInvoke function. The difference is that invoke will now always (in debug mode) throw if an enabled action isn't found, and maybeInvoke will return null if an enabled action isn't found. The invoke function can still return null as the result of the invocation, though, so I purposely didn't change the return value to Object from Object?.
parent 268adc52
...@@ -882,35 +882,24 @@ class Actions extends StatefulWidget { ...@@ -882,35 +882,24 @@ class Actions extends StatefulWidget {
/// Invokes the action associated with the given [Intent] using the /// Invokes the action associated with the given [Intent] using the
/// [Actions] widget that most tightly encloses the given [BuildContext]. /// [Actions] widget that most tightly encloses the given [BuildContext].
/// ///
/// The `context`, `intent` and `nullOk` arguments must not be null. /// This method returns the result of invoking the action's [Action.invoke]
/// method.
///
/// The `context` and `intent` arguments must not be null.
/// ///
/// If the given `intent` doesn't map to an action, or doesn't map to one that /// If the given `intent` doesn't map to an action, or doesn't map to one that
/// returns true for [Action.isEnabled] in an [Actions.actions] map it finds, /// returns true for [Action.isEnabled] in an [Actions.actions] map it finds,
/// then it will look to the next ancestor [Actions] widget in the hierarchy /// then it will look to the next ancestor [Actions] widget in the hierarchy
/// until it reaches the root. /// until it reaches the root.
/// ///
/// In debug mode, if `nullOk` is false, this method will throw an exception /// This method will throw an exception if no ambient [Actions] widget is
/// if no ambient [Actions] widget is found, or if the given `intent` doesn't /// found, or if the given `intent` doesn't map to an enabled action in any of
/// map to an action in any of the [Actions.actions] maps that are found. In /// the [Actions.actions] maps that are found.
/// release mode, this method will return null if no matching enabled action
/// is found, regardless of the setting of `nullOk`.
///
/// Setting `nullOk` to true indicates that if no ambient [Actions] widget is
/// found, then in debug mode, this method should return null instead of
/// throwing an exception.
///
/// This method returns the result of invoking the action's [Action.invoke]
/// method. If no action mapping was found for the specified intent (and
/// `nullOk` is true), or if the actions that were found were disabled, or the
/// action itself returns null from [Action.invoke], then this method returns
/// null.
static Object? invoke<T extends Intent>( static Object? invoke<T extends Intent>(
BuildContext context, BuildContext context,
T intent, { T intent,
bool nullOk = false, ) {
}) {
assert(intent != null); assert(intent != null);
assert(nullOk != null);
assert(context != null); assert(context != null);
Action<T>? action; Action<T>? action;
InheritedElement? actionElement; InheritedElement? actionElement;
...@@ -929,7 +918,7 @@ class Actions extends StatefulWidget { ...@@ -929,7 +918,7 @@ class Actions extends StatefulWidget {
}); });
assert(() { assert(() {
if (!nullOk && actionElement == null) { if (actionElement == null) {
throw FlutterError('Unable to find an action for an Intent with type ' throw FlutterError('Unable to find an action for an Intent with type '
'${intent.runtimeType} in an $Actions widget in the given context.\n' '${intent.runtimeType} in an $Actions widget in the given context.\n'
'$Actions.invoke() was unable to find an $Actions widget that ' '$Actions.invoke() was unable to find an $Actions widget that '
...@@ -951,6 +940,50 @@ class Actions extends StatefulWidget { ...@@ -951,6 +940,50 @@ class Actions extends StatefulWidget {
return _findDispatcher(actionElement!).invokeAction(action!, intent, context); return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
} }
/// Invokes the action associated with the given [Intent] using the
/// [Actions] widget that most tightly encloses the given [BuildContext].
///
/// This method returns the result of invoking the action's [Action.invoke]
/// method. If no action mapping was found for the specified intent, or if the
/// actions that were found were disabled, or the action itself returns null
/// from [Action.invoke], then this method returns null.
///
/// The `context` and `intent` arguments must not be null.
///
/// If the given `intent` doesn't map to an action, or doesn't map to one that
/// returns true for [Action.isEnabled] in an [Actions.actions] map it finds,
/// then it will look to the next ancestor [Actions] widget in the hierarchy
/// until it reaches the root.
static Object? maybeInvoke<T extends Intent>(
BuildContext context,
T intent,
) {
assert(intent != null);
assert(context != null);
Action<T>? action;
InheritedElement? actionElement;
_visitActionsAncestors(context, (InheritedElement element) {
final _ActionsMarker actions = element.widget as _ActionsMarker;
final Action<T>? result = actions.actions[intent.runtimeType] as Action<T>?;
if (result != null) {
actionElement = element;
if (result.isEnabled(intent)) {
action = result;
return true;
}
}
return false;
});
if (actionElement == null || action == null) {
return null;
}
// Invoke the action we found using the relevant dispatcher from the Actions
// Element we found.
return _findDispatcher(actionElement!).invokeAction(action!, intent, context);
}
@override @override
State<Actions> createState() => _ActionsState(); State<Actions> createState() => _ActionsState();
......
...@@ -150,6 +150,84 @@ void main() { ...@@ -150,6 +150,84 @@ void main() {
expect(result, isTrue); expect(result, isTrue);
expect(invoked, isTrue); expect(invoked, isTrue);
}); });
testWidgets('Actions widget can invoke actions with default dispatcher and maybeInvoke', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
await tester.pumpWidget(
Actions(
actions: <Type, Action<Intent>>{
TestIntent: TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
),
},
child: Container(key: containerKey),
),
);
await tester.pump();
final Object? result = Actions.maybeInvoke(
containerKey.currentContext!,
const TestIntent(),
);
expect(result, isTrue);
expect(invoked, isTrue);
});
testWidgets('maybeInvoke returns null when no action is found', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
await tester.pumpWidget(
Actions(
actions: <Type, Action<Intent>>{
TestIntent: TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
),
},
child: Container(key: containerKey),
),
);
await tester.pump();
final Object? result = Actions.maybeInvoke(
containerKey.currentContext!,
DoNothingIntent(),
);
expect(result, isNull);
expect(invoked, isFalse);
});
testWidgets('invoke throws when no action is found', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
await tester.pumpWidget(
Actions(
actions: <Type, Action<Intent>>{
TestIntent: TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
),
},
child: Container(key: containerKey),
),
);
await tester.pump();
final Object? result = Actions.maybeInvoke(
containerKey.currentContext!,
DoNothingIntent(),
);
expect(result, isNull);
expect(invoked, isFalse);
});
testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async { testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey(); final GlobalKey containerKey = GlobalKey();
bool invoked = false; bool invoked = false;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment