flutterdarttyping

List.map().toList() producing List<Set<Widget> instead of List<Widget>


I'm learning the Flutter ropes. The tutorial was great, but when I tried to replace the hardcoded sidebar switch and NavigationRail with ones generated from a list, I ran into a bunch of typing trouble. Basically, I want to generate them from this:

var appPages = [
  {
    'name': 'Header 1',
    'icon': const Icon(Icons.cloud_upload),
    'content': const PlaceholderPage(
        placeholderText: '<h1>Test Header 1</h1><br/>Lorem ipsum.')
  },
  {
    'name': 'Header 2',
    'icon': const Icon(Icons.delete_sweep_outlined),
    'content': const PlaceholderPage(
        placeholderText: '<h2>Test Header 2</h2><br/>Lorem ipsum.')
  },
  {
    'name': 'Bold Italics',
    'icon': const Icon(Icons.bloodtype),
    'content': const PlaceholderPage(
        placeholderText: '<i>Test <b>Bold</b> Italics</i><br/>Lorem ipsum.')
  },
];

Then instead of

    Widget page;
    switch (selectedIndex) {
      case 0:
        page = const PlaceholderPage(placeholderText: '<h1>Test Header 1</h1><br/>Lorem ipsum.');
        break;
      case 1:
        page = const PlaceholderPage(placeholderText: '<h2>Test Header 2</h2><br/>Lorem ipsum.');
        break;
      case 2:
        page = const PlaceholderPage(placeholderText: '<i>Test <b>Bold</b> Italics</i><br/>Lorem ipsum.');
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

I would have:

    var page = appPages[selectedIndex]['content'] as Widget?;

And instead of listing each NavigationRailDestination:

                  destinations:
                    const [
                      NavigationRailDestination(
                        icon: Icon(Icons.cloud_upload),
                        label: Text('Header 1')),
                      NavigationRailDestination(
                        icon: Icon(Icons.delete_sweep_outlined),
                        label: Text('Header 2')),
                      NavigationRailDestination(
                        icon: Icon(Icons.bloodtype),
                        label: Text('Bold Italics')),
                  ],

I'd have something like:

                  destinations:
                      appPages.map((e) => {
                            NavigationRailDestination(
                                icon: e['icon'] as Widget, label: Text(e['name'] as String))
                          }).toList() as List<NavigationRailDestination>

But currently this fails with

_TypeError (type 'List<Set<NavigationRailDestination>>' is not a subtype of type 'List<NavigationRailDestination>' in type cast)

Any fix for this error, or a more elegant way to do it?

Thanks in advance!

pubspec.yaml:

name: sidebar_test
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.3.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.0
  material_design_icons_flutter: ^7.0.0
  styled_text: ^8.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_text/styled_text.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';

var appName = 'Sidebar Test';

var appTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(seedColor: const Color(0x006AAE3F)),
);

var appStyleTags = {
  'h1': StyledTextTag(
      style: TextStyle(
          height: 2, fontSize: 50, color: appTheme.colorScheme.primary)),
  'h2': StyledTextTag(
      style: appTheme.textTheme.displayMedium!.copyWith(
    color: appTheme.colorScheme.primary,
  )),
  'b': StyledTextTag(style: const TextStyle(fontWeight: FontWeight.bold)),
  'i': StyledTextTag(style: const TextStyle(fontStyle: FontStyle.italic)),
};

var appPages = [
  {
    'name': 'Header 1',
    'icon': const Icon(Icons.cloud_upload),
    'content': const PlaceholderPage(
        placeholderText: '<h1>Test Header 1</h1><br/>Lorem ipsum.')
  },
  {
    'name': 'Header 2',
    'icon': const Icon(Icons.delete_sweep_outlined),
    'content': const PlaceholderPage(
        placeholderText: '<h2>Test Header 2</h2><br/>Lorem ipsum.')
  },
  {
    'name': 'Bold Italics',
    'icon': const Icon(Icons.bloodtype),
    'content': const PlaceholderPage(
        placeholderText: '<i>Test <b>Bold</b> Italics</i><br/>Lorem ipsum.')
  },
];

var appPagesFirst = appPages[0];
// var appPagesList = appPages.entries.toList();

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: appName,
        theme: appTheme,
        home: const HomePage(),
        debugShowCheckedModeBanner: false,
        restorationScopeId: appName,
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var selectedIndex = 0;
  bool? navExtended;

  @override
  Widget build(BuildContext context) {
    var colorScheme = Theme.of(context).colorScheme;

    // Use appPages
    var page = appPages[selectedIndex]['content'] as Widget?;

    // Instead of switch:
    // Widget page;
    // switch (selectedIndex) {
    //   case 0:
    //     page = const PlaceholderPage(placeholderText: '<h1>Test Header 1</h1><br/>Lorem ipsum.');
    //     break;
    //   case 1:
    //     page = const PlaceholderPage(placeholderText: '<h2>Test Header 2</h2><br/>Lorem ipsum.');
    //     break;
    //   case 2:
    //     page = const PlaceholderPage(placeholderText: '<i>Test <b>Bold</b> Italics</i><br/>Lorem ipsum.');
    //     break;
    //   default:
    //     throw UnimplementedError('no widget for $selectedIndex');
    // }

    // The container for the current page, with its background color
    // and subtle switching animation.
    var mainArea = ColoredBox(
      color: colorScheme.surfaceVariant,
      child: AnimatedSwitcher(
        duration: const Duration(milliseconds: 200),
        child: page,
      ),
    );

    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) {
          return Row(
            children: [
              SafeArea(
                child: NavigationRail(
                  selectedIndex: selectedIndex,
                  onDestinationSelected: (value) {
                    setState(() {
                      selectedIndex = value;
                    });
                  },
                  extended: navExtended != null
                      ? navExtended ?? true
                      : constraints.maxWidth >= 600,
                  leading: StyledText(
                    text: '<b>$appName</b>',
                    tags: appStyleTags,
                  ),
                  trailing: Expanded(
                    child: Align(
                      alignment: Alignment.bottomLeft,
                      child: IconButton(
                        icon: Icon((navExtended ?? true)
                            ? MdiIcons.arrowCollapseLeft
                            : MdiIcons.arrowExpandRight),
                        onPressed: () {
                          setState(() {
                            setState(
                                () => navExtended = !(navExtended ?? true));
                          });
                        },
                      ),
                    ),
                  ),
                  destinations:
                      // Use appPages
                      appPages.map((e) => {
                            NavigationRailDestination(
                                icon: e['icon'] as Widget, label: Text(e['name'] as String))
                          }).toList() as List<NavigationRailDestination>
                      // Instead of
                      // const [
                      //   NavigationRailDestination(
                      //     icon: Icon(Icons.cloud_upload),
                      //     label: Text('Header 1')),
                      //   NavigationRailDestination(
                      //     icon: Icon(Icons.delete_sweep_outlined),
                      //     label: Text('Header 2')),
                      //   NavigationRailDestination(
                      //     icon: Icon(Icons.bloodtype),
                      //     label: Text('Bold Italics')),
                      // ],
                ),
              ),
              Expanded(child: mainArea),
            ],
          );
          // }
        },
      ),
    );
  }
}

class PlaceholderPage extends StatelessWidget {
  const PlaceholderPage({
    super.key,
    required this.placeholderText,
  });

  final String placeholderText;

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      const Spacer(),
      Card(
        child: Padding(
            padding: const EdgeInsets.all(20),
            child: AnimatedSize(
                duration: const Duration(milliseconds: 200),
                child: StyledText(text: placeholderText, tags: appStyleTags))),
      ),
      const Spacer(),
    ]);
  }
}

Solution

  • Dart's anonymous functions are defined with parentheses and curly braces: (s) { print(s); }, but if it consists of a single statement, you can use right arrow ((s) => print(s);, and then you must omit the curly braces or things will break, or worse, be interpreted as a Set. D'oh.

    Removing them (or the arrow) was all that was needed:

                      destinations:
                          appPages.map((e) =>
                                NavigationRailDestination(
                                    icon: e['icon'] as Widget, label: Text(e['name'] as String))
                              ).toList() as List<NavigationRailDestination>
    

    Working sandbox: https://flutlab.io/editor/2041e217-a3b9-482f-bc3e-45c0a36895fd