Search code examples
flutterdartstrong-typingflutter-navigation

Any way to have strongly typed use of Navigator?


I have a MaterialButton with the following onPressed field:

onPressed: () async {
    final int intResult = await showMyDialog(context, provider.myArg) ?? 0;
    //...
}

Here is the showMyDialog function:

Future<int?> showMyDialog(BuildContext context, Object someArg) async {
  return showDialog<int>(
    context: context,
    builder: (BuildContext context) {
      return ChangeNotifierProvider<MyProvider>(
        create: (_) => MyProvider(someArg),
        child: MyDialog(),
      );
    },
  );
}

Now the problem I see is that in MyDialog (a StatelessWidget), I need to use Navigator.pop(...) to return a value for the awaited intResult. However, I can't seem to find a way to strongly type these calls, and so it's hard to be certain that no runtime type error will happen.

The best I have right now, which is admittedly a bit inconvenient, is to subclass StatelessWidget and wrap the Navigator functions in it:

abstract class TypedStatelessWidget<T extends Object> extends StatelessWidget {
  void pop(BuildContext context, [T? result]) {
    Navigator.pop(context, result);
  }
}

Then in a TypedStatelessWidget<int> we can use pop(context, 0) normally, and pop(context, 'hi') will be marked as a type error by the editor. This still doesn't link the dialog return type and the navigator, but at least it avoids manually typing each navigator call.


Is there any better way to have this strongly typed ?


Solution

  • So here's what I ended up doing. Essentially, this allows for a widget type and a provider type to be "tied together". This way, when the custom pop function is called in the dialog, there is a type contract (see PopType) for what the return value should be. There is also support for non-manual pop navigation by using WillPopScope, which gets the typed provider's return function and arguments (see the abstract functions in TypedProvider) to properly pass that value up the widget tree.

    mixin TypedWidget<T, N extends TypedProvider<T>?> on Widget {
      ChangeNotifierProvider<N> asTypedWidget(
          BuildContext context, N Function(BuildContext) createProviderFunc) {
        return ChangeNotifierProvider<N>(
            create: createProviderFunc,
            builder: (BuildContext innerContext, _) {
              return WillPopScope(
                  child: this,
                  onWillPop: () {
                    final N provider = Provider.of<N>(innerContext, listen: false);
                    if (provider == null) {
                      return Future<bool>.value(true);
                    }
                    provider.onPopFunction(provider.getPopFunctionArgs());
                    return Future<bool>.value(true);
                  });
            });
      }
    
      /// Builds this TypedWidget inside `showDialog`.
      ///
      /// The `TypedProvider`'s `onPopFunction` is called if the
      /// dialog is closed outside of a manual `Navigator.pop()`. This doesn't have
      /// a return type; all returning actions should be done inside the defined
      /// `onPopFunction` of the provider.
      ///
      /// Example:
      /// ```
      /// MyTypedWidget().showTypedDialog(
      ///   context,
      ///   (BuildContext context) => MyTypedProvider(...)
      /// );
      /// ```
      Future<void> showTypedDialog(
          BuildContext context, N Function(BuildContext) createProviderFunc,
          {bool barrierDismissible = true}) async {
        await showDialog<void>(
          context: context,
          barrierDismissible: barrierDismissible,
          builder: (_) => asTypedWidget(context, createProviderFunc),
        );
      }
    }
    
    abstract class TypedProvider<PopType> with ChangeNotifier {
      TypedProvider(this.onPopFunction);
    
      Function(PopType) onPopFunction;
      PopType getPopFunctionArgs();
    
      void pop(BuildContext context) {
        onPopFunction(getPopFunctionArgs());
        Navigator.pop(context);
      }
    }
    

    There are most likely other ways to achieve that, and this solution certainly has some constraints, but it effectively provides strong typing to the dialog.