Skip to content

PopupMenuButton asserts when rendering tooltip after navigating back using Cupertino tab layout #167359

@fredecodes

Description

@fredecodes

Steps to reproduce

  1. Install Flutter 3.27 with this commit applied (just a one line change): 4503f2f
  2. Run the attached sample app on MacOS or iOS.
  3. Click "Go to Second Page" button
  4. Click the back button at top left, and don't move the cursor
  5. App will assert as soon as the page transition completes if cursor is in position to invoke the PopupMenu's tooltip

Note 1: If you don't apply the aforementioned commit, you will instead crash when attempting to use the PopupMenu after navigation. That issue was fixed by the commit and that bug has been closed.
Note 2: If you set the tooltip for the PopupMenu to empty string, the assert does not occur (see commented out code in example)

Expected results

App doesn't crash/assert

Actual results

App asserts when rendering the tooltip for the PopupMenu. Top of call stack shown here:
_AssertionError._doThrowNew (flutter/bin/cache/pkg/sky_engine/lib/_internal/vm/lib/errors_patch.dart:63)
_AssertionError._throwNew (flutter/bin/cache/pkg/sky_engine/lib/_internal/vm/lib/errors_patch.dart:45)
RenderBox.size (flutter/packages/flutter/lib/src/rendering/box.dart:2251)
RenderFractionalTranslation.applyPaintTransform (flutter/packages/flutter/lib/src/rendering/proxy_box.dart:2974)
RenderObject.getTransformTo (flutter/packages/flutter/lib/src/rendering/object.dart:3520)
RenderBox.localToGlobal (flutter/packages/flutter/lib/src/rendering/box.dart:3082)
TooltipState._buildTooltipOverlay (flutter/packages/flutter/lib/src/material/tooltip.dart:793)
Builder.build (flutter/packages/flutter/lib/src/widgets/basic.dart:7818)

Code sample

Code sample
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      home: const CupertinoHomeScaffold(),
    );
  }
}

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

  void _handleMenuSelection(String value) {
    debugPrint('Menu selection: $value');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text('Home Page'),
        leading: PopupMenuButton<String>(
          icon: const Icon(Icons.more_vert),
          //tooltip: '', // empty tooltip prevents the crash
          onSelected: _handleMenuSelection,
          itemBuilder:
              (BuildContext context) => [
                const PopupMenuItem<String>(
                  value: 'settings',
                  child: Text('Settings'),
                ),
                const PopupMenuItem<String>(
                  value: 'about',
                  child: Text('About'),
                ),
              ],
        ),
      ),
      body: Center(
        child: TextButton(
          onPressed:
              () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const SimplePage(title: 'Second Page'),
                ),
              ),
          child: const Text('Go to Second Page'),
        ),
      ),
    );
  }
}

class SimplePage extends StatelessWidget {
  const SimplePage({super.key, required this.title});
  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: const Center(child: Text('Nothing to see here.')),
    );
  }
}

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

  @override
  CupertinoHomeScaffoldState createState() => CupertinoHomeScaffoldState();
}

class CupertinoHomeScaffoldState extends State<CupertinoHomeScaffold> {
  CupertinoTabController controller = CupertinoTabController();

  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      controller: controller,
      tabBar: CupertinoTabBar(
        key: const Key(Keys.tabBar),
        items: TabItem.values.map((ti) => _buildItem(ti)).toList(),
        onTap:
            (index) => setState(() {
              controller.index = index;
            }),
      ),
      tabBuilder: (context, index) {
        final tabItem = TabItem.values[index];
        return CupertinoTabView(
          navigatorKey: navigatorKeys[tabItem],
          builder:
              (context) =>
                  tabItem == TabItem.incidents
                      ? const MyHomePage()
                      : const SimplePage(title: 'Responders'),
        );
      },
    );
  }

  BottomNavigationBarItem _buildItem(TabItem tabItem) {
    final itemData = TabItemData.allTabs[tabItem]!;
    final color =
        controller.index == tabItem.index ? Colors.indigo : Colors.grey;
    return BottomNavigationBarItem(
      icon: Icon(itemData.icon, color: color),
      label: itemData.title,
    );
  }
}

enum TabItem { incidents, responders }

class Keys {
  static const String tabBar = 'tabBar';
  static const String incidentsTab = 'incidentsTab';
  static const String respondersTab = 'respondersTab';
}

final Map<TabItem, GlobalKey<NavigatorState>> navigatorKeys = {
  TabItem.incidents: GlobalKey<NavigatorState>(),
  TabItem.responders: GlobalKey<NavigatorState>(),
};

class TabItemData {
  const TabItemData({
    required this.key,
    required this.title,
    required this.icon,
  });

  final String key;
  final String title;
  final IconData icon;

  static const Map<TabItem, TabItemData> allTabs = {
    TabItem.incidents: TabItemData(
      key: Keys.incidentsTab,
      title: 'Incidents',
      icon: Icons.work,
    ),
    TabItem.responders: TabItemData(
      key: Keys.respondersTab,
      title: 'Responders',
      icon: Icons.person,
    ),
  };
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.29.2, on macOS 15.3.2 24D81 darwin-arm64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 16.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.3)
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: error messageError messages from the Flutter frameworkc: regressionIt was better in the past than it is nowf: material designflutter/packages/flutter/material repository.found in release: 3.32Found to occur in 3.32frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionteam-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions