I'm new to Flutter and have some performance concerns. For my app, I have created a custom sidebar menu, inspired by
For this purpose, I created a stateful top-level widget that acts as the parent screen. It contains a Stack
widget with the navigation screen on the button, and a content screen on top. The user should be able to open/close the menu in two ways:
- By pressing the hamburger menu icon at the top left of the content screen (either when it is fully opened, or moved to the side as in the first pic)
- By swiping right when the menu is open, and left when the menu is closed.
To satisfy point 2, I added a GestureDetector
on the parent screen, such that the swipes are detected in the entire screen, which animates the content screen to the side/back in full view. To satisfy point 1, I pass an onPress
callBack to the content screen (which passes it to the hamburger iconButton), which also does the top level animation. However, reading the documentation (stateful performance considerations), it seems that such a top-level stateful widget can be harmful for performance, as the rebuild passes down. I can't make my content screen a const
widget (which is a proposed solution) because of the callback. This is obviously suboptimal, since in the content screen, only the icon has an animated change when the menu opens (the icon changes from a hamburger to an arrow).
How can I minimize the number of rerenders in the subtree? Is there a way to pass the screen as a const
widget, even though it has a callback? Or is the current approach satisfactory?
The code, as I have it currently, is as follows:
class ParentScreen extends StatefulWidget {
const ParentScreen({Key? key}) : super(key: key);
@override
_ParentScreenState createState() => _ParentScreenState();
}
class _ParentScreenState extends State<ParentScreen> {
bool isMenuOpen = false;
double xOffset = 0;
double yOffset = 0;
double rotationAngle = 0;
double scaleFactor = 1;
double toRadians(double degrees) => degrees * math.pi / 180.0;
void animateMenu() {
setState(() {
...
isMenuOpen = !isMenuOpen;
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
// Detect user swipe to navigate between the screens
child: GestureDetector(
onHorizontalDragEnd: (DragEndDetails details) {
if (details.primaryVelocity != null) {
if (details.primaryVelocity! > 0) {
// Right swipe, close menu if open
if (isMenuOpen) animateMenu();
} else if (details.primaryVelocity! < 0) {
// Left swipe, open menu if closed
if (!isMenuOpen) animateMenu();
}
}
},
child: Scaffold(
body: Stack(
children: <Widget>[
const DrawerScreen(), // Screen with navigation information
AnimatedContainer(
transform: Matrix4.translationValues(xOffset, yOffset, 0)
..scale(scaleFactor)
..rotateZ(toRadians(rotationAngle)),
duration: const Duration(milliseconds: 300),
child: HomeScreen( // Screen with custom content
onMenuPress: animateMenu,
),
),
],
),
),
),
);
}
}