Tareq Ghassan
Tareq Ghassan

Reputation: 11

Flutter Generics in Navigation: Function Parameters Treated as dynamic at Runtime

I'm implementing a centralized navigation manager (KNavigator) to handle typed navigation with generics. However, when passing functions that accept a generic type (e.g., displayItem: (T) => String), Dart treats them as dynamic, leading to type errors.

I get the following error:

Unhandled Exception: type '(Accounts) => String' is not a subtype of type '(dynamic) => String'

Expected Behavior

  1. displayItem: (T) => String should work without explicit casting.
  2. pushNamed() should correctly infer T without treating function parameters as dynamic.
  3. No need to explicitly cast generics at runtime.

Here is my KNavigator:


part of 'route.dart';

/// [KNavigator] a class where handle navigation cross the App
class KNavigator {
  const KNavigator._();

  static final GlobalKey<NavigatorState> navigatorKey =
      GlobalKey<NavigatorState>();

  static final KNavigatorObserver _navigationObserver = KNavigatorObserver();

  /// The name of the route that loads on app startup
  static const String initialRoute = KRoutes.splashScreen;

  /// [observers] getter method
  static List<NavigatorObserver> get observers => [_navigationObserver];

  static MaterialPageRoute<dynamic> generateRoute<T>(RouteSettings settings) {
    switch (settings.name) {
      case KRoutes.selectionPage:
        final args = settings.arguments! as SelectionPageArguments<T>;
        return _setPage<T>(
          page: SelectionPage<T>(
            title: args.title,
            items: args.items,
            displayItem: args.displayItem,
            displaySubtitle: args.displaySubtitle,
            leading: args.leading,
            trailing: args.trailing,
          ),
          settings: settings,
        );
      default:
        return _errorRoute();
    }
  }

  /// [_errorRoute] in case no route found
  static MaterialPageRoute<T> _errorRoute<T>() {
    return MaterialPageRoute<T>(
      builder: (_) => Scaffold(
        appBar: AppBar(
          title: const Text('Unknown Route'),
        ),
        body: const Center(
          child: Text('Unknown Route'),
        ),
      ),
    );
  }

  /// [_setPage] which set the new screen into the material app
  static MaterialPageRoute<T> _setPage<T>({
    required Widget page,
    required RouteSettings settings,
  }) {
    return MaterialPageRoute<T>(
      builder: (_) => page,
      settings: settings,
    );
  }

  /// [pushNamed] push to a new route
  static Future<T?> pushNamed<T extends Object?>(
    String routeName, {
    dynamic args,
  }) {
    return navigatorKey.currentState!.pushNamed<T>(routeName, arguments: args);
  }

}

This is how I'm navigating to SelectionPage

 /// [onTapSelectAccount] to navigate to select page with account list in
  /// there
  static Future<Accounts?> onTapSelectAccount(
    BuildContext context,
    String account,
    List<Accounts> accountsList,
  ) async {
    final selectedAccount = await KNavigator.pushNamed<Accounts?>(
      KRoutes.selectionPage,
      args: SelectionPageArguments<Accounts>(
        title: account,
        items: accountsList,
        displayItem: (account) => account.accNickName.isEmpty
            ? account.accountName!
            : account.accNickName,
        displaySubtitle: (account) => Text(account.accountID!),
        trailing: (account) => Text(
          '${account.currencyCode!} ${FormatUtil.formatBalance(
            account.balance!,
            currencyCode: account.currencyCode!,
          )}',
        ),
      ),
    );

    return selectedAccount;
  }
}

This is my generic argument class

class SelectionPageArguments<T> {
  const SelectionPageArguments({
    required this.title,
    required this.items,
    required this.displayItem,
    this.displaySubtitle,
    this.leading,
    this.trailing,
  });

  /// Title for the selection screen
  final String title;

  /// List of selectable items
  final List<T> items;

  /// Function to format how each item should be displayed
  final String Function(T) displayItem;

  /// Function to format how each subtitle should be displayed
  final Widget? Function(T)? displaySubtitle;

  /// Optional function to provide a leading widget for each item
  final Widget? Function(T)? leading;

  /// Optional function to provide a trailing widget for each item
  final Widget? Function(T)? trailing;
}

and finally this is my SelectionPage


/// [SelectionPage] this is a custom Screen widget that represent the Select
/// screen
class SelectionPage<T> extends StatelessWidget {
  /// Creates a [SelectionPage]
  const SelectionPage({
    required this.title,
    required this.items,
    required this.displayItem,
    this.displaySubtitle,
    this.leading,
    this.trailing,
    super.key,
  });

  /// Title for the selection screen
  final String title;

  /// List of selectable items
  final List<T> items;

  /// Function to format how each item should be displayed
  final String Function(T) displayItem;

  /// Function to format how each subtitle should be displayed
  final Widget? Function(T)? displaySubtitle;

  /// Optional function to provide a leading widget for each item
  final Widget? Function(T)? leading;

  /// Optional function to provide a trailing widget for each item
  final Widget? Function(T)? trailing;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBarLinearGradient(
        title: title,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: ListView.separated(
          itemCount: items.length,
          separatorBuilder: (context, index) => const Divider(),
          itemBuilder: (context, index) {
            final item = items[index];
            return ListTile(
              onTap: () => Navigator.of(context).pop(item),
              title: Text(displayItem(item)),
              subtitle: displaySubtitle?.call(item),
              leading: leading?.call(item),
              trailing: trailing?.call(item),
            );
          },
        ),
      ),
    );
  }
}

My Question

  1. Why does Dart treat displayItem: (T) => String as (dynamic) => String at runtime?
  2. How can I ensure function parameters retain their generic type across navigation?
  3. Is there a way to make generateRoute() handle generics properly without explicitly setting ?

If I explicitly replace with , everything works fine:

      case KRoutes.selectionPage:
        final args = settings.arguments! as SelectionPageArguments<Accounts>;
        return _setPage<Accounts>(
          page: SelectionPage<Accounts>(
            title: args.title,
            items: args.items,
            displayItem: args.displayItem,
            displaySubtitle: args.displaySubtitle,
            leading: args.leading,
            trailing: args.trailing,
          ),
          settings: settings,
        );

Upvotes: 0

Views: 25

Answers (0)

Related Questions