Repository navigation
Closed
flutter/packages
#13010Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listfound in release: 3.38Found to occur in 3.38Found to occur in 3.38found in release: 3.40Found to occur in 3.40Found to occur in 3.40has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagesmaterial_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.team-designOwned by Design Languages teamOwned by Design Languages teamtriaged-designTriaged by Design Languages teamTriaged by Design Languages team
Description
Steps to reproduce
- Use a NavigationDrawer with multiple indexes alongside e.g. an IndexedStack
- Change the index from inside a page instead of the navigation items
Expected results
A single highlight moving correctly as the selected page changes.
Actual results
When jumping over, the first NavigationDrawerDestination remains highlighted. The third option uses the correct selectedIcon, but has no highlighting.
If the third option is unavailable at first, but added programmatically at the same time, both options become highlighted.
The highlighting corrects itself when the window is resized.
Code sample
Code sample
import 'package:flutter/material.dart';
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: .fromSeed(seedColor: Colors.lightBlue),
splashColor: Colors.transparent,
navigationDrawerTheme: NavigationDrawerThemeData(
indicatorShape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.elliptical(10, 10)),
),
),
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class FirstWidget extends StatelessWidget {
const FirstWidget({super.key, required this.jumpToThird});
final void Function() jumpToThird;
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: jumpToThird,
child: Text("Jump to third"),
),
);
}
}
class HelloWidget extends StatelessWidget {
const HelloWidget({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
"Hello World!",
style: Theme.of(context).textTheme.titleLarge,
),
);
}
}
class ThirdWidget extends StatelessWidget {
const ThirdWidget({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
"The third widget.",
style: Theme.of(context).textTheme.titleLarge,
),
);
}
}
class Destination {
const Destination(this.label, this.icon, this.selectedIcon);
final String label;
final Widget icon;
final Widget selectedIcon;
}
const List<Destination> destinations = [
Destination("First", Icon(Icons.history), Icon(Icons.history_outlined)),
Destination("Second", Icon(Icons.search), Icon(Icons.search_outlined)),
];
const Destination resultsDestination = Destination(
"Third",
Icon(Icons.reset_tv),
Icon(Icons.reset_tv_outlined),
);
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
int screenIndex = 0;
final thirdVisible = ValueNotifier(true); // set this to false to see the other variant
@override
void initState() {
super.initState();
thirdVisible.addListener(() => setState(() {}));
}
void jumpToThird() {
setState(() {
thirdVisible.value = true;
screenIndex = 2;
});
}
@override
Widget build(BuildContext context) {
final destWidgets = <Widget>[Padding(padding: EdgeInsets.all(8))];
for (final destination in destinations) {
destWidgets.add(
NavigationDrawerDestination(
icon: destination.icon,
label: Text(destination.label),
selectedIcon: destination.selectedIcon,
),
);
destWidgets.add(Padding(padding: EdgeInsets.all(4)));
}
if (thirdVisible.value) {
destWidgets.add(
NavigationDrawerDestination(
icon: resultsDestination.icon,
label: Text(resultsDestination.label),
selectedIcon: resultsDestination.selectedIcon,
),
);
}
final frames = <Widget>[
FirstWidget(jumpToThird: jumpToThird),
HelloWidget(),
if (thirdVisible.value) ThirdWidget(),
];
return Scaffold(
body: SafeArea(
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: NavigationDrawer(
selectedIndex: screenIndex,
onDestinationSelected: (value) =>
setState(() => screenIndex = value),
children: destWidgets,
),
),
VerticalDivider(),
Expanded(
child: IndexedStack(index: screenIndex, children: frames),
),
],
),
),
);
}
}
Screenshots or Video
Logs
Logs
Launching lib/main.dart on Linux in debug mode...
✓ Built build/linux/x64/debug/bundle/simagefbug
(com.example.simagefbug:16985): Atk-CRITICAL **: 10:15:59.255: atk_socket_embed: assertion 'plug_id != NULL' failed
Gdk-Message: 10:15:59.316: Unable to load from the cursor theme
Connecting to VM Service at ws://127.0.0.1:35039/C4nmcnsfdos=/ws
Connected to the VM Service.Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.38.5, on Arch Linux 6.18.2-arch2-1, locale en_US.UTF-8) [41ms]
• Flutter version 3.38.5 on channel stable at /home/gert/development/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision f6ff1529fd (3 weeks ago), 2025-12-11 11:50:07 -0500
• Engine revision 1527ae0ec5
• Dart version 3.10.4
• DevTools version 2.51.1
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
omit-legacy-version-file, enable-lldb-debugging
[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,793ms]
• Android SDK at /home/gert/Android/Sdk
• Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A)
• Platform android-36, build-tools 36.0.0
• ANDROID_HOME = /home/gert/Android/Sdk
• Java binary at: /home/gert/development/android-studio/jbr/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Chrome - develop for the web [9ms]
• CHROME_EXECUTABLE = chromium
[✓] Linux toolchain - develop for Linux desktop [444ms]
• clang version 21.1.6
• cmake version 4.2.1
• ninja version 1.13.2
• pkg-config version 2.5.1
• OpenGL core renderer: Mesa Intel(R) UHD Graphics (TGL GT1)
• OpenGL core version: 4.6 (Core Profile) Mesa 25.3.2-arch1.1
• OpenGL core shading language version: 4.60
• OpenGL ES renderer: Mesa Intel(R) UHD Graphics (TGL GT1)
• OpenGL ES version: OpenGL ES 3.2 Mesa 25.3.2-arch1.1
• OpenGL ES shading language version: OpenGL ES GLSL ES 3.20
• GL_EXT_framebuffer_blit: yes
• GL_EXT_texture_format_BGRA8888: yes
[✓] Connected device (2 available) [185ms]
• Linux (desktop) • linux • linux-x64 • Arch Linux 6.18.2-arch2-1
• Chrome (web) • chrome • web-javascript • Chromium 143.0.7499.169 Arch Linux
[✓] Network resources [1,148ms]
• All expected network resources are available.
! Doctor found issues in 1 category.Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listfound in release: 3.38Found to occur in 3.38Found to occur in 3.38found in release: 3.40Found to occur in 3.40Found to occur in 3.40has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagesmaterial_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.team-designOwned by Design Languages teamOwned by Design Languages teamtriaged-designTriaged by Design Languages teamTriaged by Design Languages team
Activity
tirth-patel-nc commentedon Jan 5, 2026
Thanks for the report. Seeing the same behaviour with latest SDK versions.
dkwingsmt commentedon Mar 25, 2026
I've investigated this issue and it was quite a rabbit hole.
I appreciate the mention of how resizing the window makes the paint appear correctly. I've tested it further: If the mouse hovers over the item as soon as you click the button, the paint jumps to a "middle value" and stops there. Apparently the animation object is being updated, but the repaint signal is not transmitted to the correct place.
The following section is a summary of the cause. It was drafted with the assistance of AI, because there are a few details that I'm not completely clear yet.
The root cause is a repaint signal decoupling between the local animation and the remote painting canvas. Here is the technical breakdown:
Remote Rendering via
Ink:Following PR Fix NavigationBar indicator overlay color #164484,
NavigationIndicatornow utilizes theInkwidget. Unlike standard widgets,Inkdoes not paint on its own localRenderObject. Instead, it identifies the nearestMaterialancestor and delegates the painting to that ancestor’s canvas. InNavigationDrawer, thisMaterialwidget sits outside theListView.Local vs. Remote Layer Mismatch:
When the
selectedIndexchanges, theAnimatedBuilderupdates theTransformwidget. This callsmarkNeedsPaint()on theTransform's local layer. However, becauseInkis a "remote" painter, theTransformis effectively scaling an empty area on its local layer. The actual pixels belong to theMateriallayer, which is unaware that a transformation is occurring deep inside theListView's sub-hierarchy.The
ListViewSignal Barrier:Even with
addRepaintBoundaries: false, aListView(and its internalViewport) creates a complex rendering structure. The "update signal" from a local animation doesn't automatically propagate upward to theMaterialancestor. Since theInkwidget itself is often treated as a static element within theListView's cache, the rendering engine sees no reason to tell the distantMaterialcanvas: "Hey, you need to redraw your Ink decorations because this specific child has changed."Manual Override (Hover/Click):
The reason clicking or hovering "fixes" the frame is that
InkWell(used for the ripple/hover effect) is hardcoded to callmarkNeedsPaint()directly on theMaterialcontroller. This "wake-up call" forces theMaterialcanvas to finally check the current state of all delegatedInkfeatures, which is why the animation "jumps" to the current value once you interact with it.I've verified that the following change fixes the problem:
This forces that the closest
Materialis updated every timeanimationchanges, which I think also makes sense. Although I'm not sure if this is an idiomatic enough way to writeMaterialrelated painting.cc @QuncCccccc @bleroux Maybe you have more insight and have more say in deciding whether this is an acceptable fix.
QuncCccccc commentedon Apr 8, 2026
Sorry for the late response. I think the fix makes sense to me!
theprantadutta commentedon Jun 10, 2026
Since this has been sitting for a couple of months with the fix already worked out, I'd like to pick it up. Plan: apply the
Material.of(context).markNeedsPaint()approach @dkwingsmt outlined (which @QuncCccccc said makes sense), add a regression test that changesselectedIndexprogrammatically and verifies the indicator repaints on the ancestor Material, and open a PR tagging you both for review. If you'd rather see a different mechanism than calling markNeedsPaint from the builder, I'm glad to iterate on that in review.dkwingsmt commentedon Jun 10, 2026
@theprantadutta Sounds good! LMK when the PR is ready.
theprantadutta commentedon Jun 10, 2026
PR is ready: #187802 — it applies the fix you verified, with a note on why
markNeedsPaint()seems idiomatic here (it's part of the publicMaterialInkControllercontract and the same mechanism ink features use). The regression test pumps toEnginePhase.layoutso it observes the missing repaint signal directly rather than forcing a paint; it fails without the fix.JonoGrubb commentedon Jul 27, 2026
We hit this in production (web) and can offer a smaller regression case plus a sharper characterization that may help whoever picks up the fix.
Two-widget minimal repro — freezes 100% of the time, no navigation drawer or selection machinery needed:
The indicator never visually changes (0 distinct frames over 12s of automated screenshots), while the same tree without the
RepaintBoundary— or with the pre-#164484 non-Inkindicator — animates normally. InNavigationDrawerthe boundary comes for free from theListView'saddRepaintBoundaries, which is why the drawer is the reported surface;NavigationBar(Row, no boundary) mostly self-heals but we still caught it stale in 1 of 12 automated browser-back runs.Two observations that pinned this down for us, consistent with @dkwingsmt's analysis:
Navigator+ zero-duration routes and with a bare timer-drivensetState— no Router/go_router involvement (a defaultMaterialPageRoutetransition masks it by repainting everything for ~300ms).Externally we're carrying the same workaround shape as #187802 (a scoped per-frame
markNeedsPaintover the host Material for the animation window) and it takes our automated browser-back stale rate from 12/12 to 0/12, which is decent evidence the PR's mechanism is the right one. Happy to test a build of #187802 against our harness if useful.[material_ui] Repaint NavigationIndicator when the selection changes …
github-actions commentedon Oct 16, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.