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,
),
),
],
),
),
),
);
}
}
Well, you don't need to make the parent widget a stateful widget.
First make the actual menu including it's animation it's on widget which draws over everything else.. (Similar to what you said in a Stack widget).
Then create a object (typically called a BLoC in flutter-world) which lives outside the widget tree - either a ChangeNotifier or a Stream and inject it into the stateless widgets (easiest is by using the provider package, but you can also use an InheritedWidget.
When you want to show the menu you would just change the state of this external object which will notify the menu widget to expand.
Related
I have 3 views which are accessible via the bottom navigation tab. Each view has its own ListView, which looks like this:
// primary = bottomTabNavigation.index //
ListView(
controller: primary ? null : scrollController,
key: const PageStorageKey<String>('view1'),
primary: primary,
physics: primary
? AlwaysScrollableScrollPhysics()
: NeverScrollableScrollPhysics(),
children: const [
Text("A"),
SizedBox(height: 1000),
Text("B"),
],
),
If I start a big swipe on view1, and switch to view2 via bottom tab navigator, the scroll position when I come back to view1 is still at the top. Somehow, the scroll position only saves upon the scrolling animation completing.
Is there some way to switch tabs and store the last position (without waiting for animation)?
Create a Key outside the build method
final _key = GlobalKey();
Step 1: make your widgets staefulWidget.
Step 2: now use AutomaticKeepAliveClientMixin using with keyword.
class _DealListState extends
State<DealList> with
AutomaticKeepAliveClientMixin<DealList>
{
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
// your current widget build
}}
It will keep your listview and other states when you moves from one page to another.
Note: if it's impossible to change every page to stateful widget then just make a new StatefulWidget that use AutomaticKeepAliveClientMixin and will take a child widget from outside and now you can use this widget to wrap your already present widget and can be used through the app.
I have a list of checkbox items (PopupMenuItem) inside my popup menu, which is triggered by a popupMenuButton. I want the user to be able to select a number of checkboxes, but as soon as one item is selected it closes the window.
Is there any way to prevent this? I need it to stay open, or alternatively force it open again immediately.
(I tried creating my own PopupItem class to override the "handleTap()", but I need to update the state of the parent menu view, which I can't call from another class. So Ive removed that again.)
class TopicsNotificationMenu extends StatefulWidget {
List<Topic> topics = [];
TopicsNotificationMenu(this.topics);
#override
_TopicsNotificationMenuState createState() =>
_TopicsNotificationMenuState();
}
class _TopicsNotificationMenuState extends State<TopicsNotificationMenu> {
_TopicsNotificationMenuState();
_updateTopics(_tp){
setState(() {
if(_tp.value == true){
_tp.value = false;
}else{
_tp.value = true;
_registerTopic(_tp.name);
}
});
}
#override
Widget build(BuildContext context) {
return PopupMenuButton(
onSelected: (value) {
_updateTopics(value);
},
itemBuilder: (BuildContext context) {
return widget.topics.map((var tp) {
var _icon = (tp.value == true) ? Icons.check_box : Icons.check_box_outline_blank;
return PopupMenuItem(
value: tp,
child: ListTile(
leading: Icon(_icon),
title: Text(tp.name),
),
);
}).toList();
});
}
You can try to reopen it each time after the user's selection. I made an example here
Alternatively, I would advise creating your own widget with the desired behaviour.
I had to create my own widget for this. In summary, I wanted a floating list in the top-right of my screen with a list of checkboxed items. When I press the items they become checked/unchecked but the window remains open until I click off it. I used the following widget tree:
An OverlayEntry widget so that I could place it anywhere floating above the app
-> added the SafeArea widget so that my padding would include the notification bar at the top of the phone
-> a Gesture Detector, so that on tapping off it I could close it
-> a column with CrossAxisAlignment.end so that it was placed in the top-right
-> a container widget with some padding
-> a Material for elevation shading and to contain a list
-> The Listview
-> The List Tiles with icon and Text for each item. The icon was either the ticked or unticked graphic, depending on it's value (which is stored as a boolean in an array)
onTap of a ListTile it updated the state of the widget and dispayed it again. There is no visual blinking, it is instant.
onTap of the Gesture Detector it simply removes the widget.
Totally new to flutter, less than 10h on it.
I have a PageView handing 3 pages, it scrolls horizontally through them perfectly.
But I would like to change the "allowed swipe" area. I don't want to let the user change the pages scrolling from any position of the page, but instead, for example, just let him scroll the pages if he swipes on the AppBar component for example.
I saw the PageView has the applyTo method, I'm just lost about how to give to it the ID(keys) or the appBar to see if this will work.
Is there a way of achieving this "only scroll if the user swipes on the component X"?
Edit 1
Solution suggested by Alberto Miola works like a charm, here is my code (I had to implement from PreferredSizeWidget since is required to modify the AppBar).
class GestureDetectorForAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
final AppBar appbar;
final GestureDragUpdateCallback onPanUpdate;
GestureDetectorForAppBar({
Key key,
this.appbar,
this.onPanUpdate,
#required this.height,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(child: this.appbar, onPanUpdate: this.onPanUpdate,);
}
#override
Size get preferredSize => Size.fromHeight(height);
}
First of all, you need to "block" the swipe gesture on the PageView itself using NeverScrollableScrollPhysics:
PageView(
physics: const NeverScrollableScrollPhysics(),
children: [...],
controller: ...
);
Note the usage of a const constructor. In this way you won't be able to move among pages with your finger. In Flutter you have widgets and not components.
Give a look at the GestureDetector widget which can be used to listen on swipes and change the currently visible content of your PageView. It can be used to detect swipes:
GestureDetector(
onPanUpdate: (data) {
if (data.delta.dx > 0) {
// right swipe
}
if (data.delta.dx < 0) {
// right swipe
}
}
);
In order, I suggest you to first read about NeverScrollableScrollPhysics() in the official documentation. It's used to "block" scrolling behaviors. Then, use GestureDetector() to wrap the widget you want to be used as a "scroll director" (the one that actually scrolls the pages).
Inside onPanUpdate you'll deal with the animateToPage method of the PageController to change the currently visible page.
I'm using this reorderables package. This package works by having a list of children widgets that are each wrapped with a Draggable and put inside a DragTarget. Before that the childs key is assigned to a GlobalObjectKey.
After the dragTarget is created, it is assigned(or rebuild?) to a KeyedSubTree:
dragTarget = KeyedSubtree(key: keyIndexGlobalKey, child: dragTarget);
According to the comments in the package source code, this should preserve the child widgets state (toWrap) when being dragged:
// We pass the toWrapWithGlobalKey into the Draggable so that when a list
// item gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap.key);
The reordering itself happens not with the DragTarget accepting the Draggable dragged into it, but rather by using the DragTarget around each child to get index of the current position the Draggable is hovering over. When the Draggable is let go, a reorder function will get called, which removes the widget (that was being dragged) from the list and inserting it into the new position.
Now comes my problem: The state of the widget is not being preserved. I made a simple TestWidget to test this:
class TestWidget extends StatefulWidget{
#override
_TestWidgetState createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
Color boxColor;
#override
void initState() {
super.initState();
boxColor= Colors.blue;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: BoxDecoration(color: boxColor),
child: Text("Test"),
),
FlatButton(
onPressed: (){
setState(() {
boxColor = Colors.red;
});
},
padding: EdgeInsets.all(8.0),
child: Text("Change to Red"),
color: Colors.grey,
)
],
);
}
}
This widget has a Container with a initial blue background (boxColor) and a button. When the button is pressed, it will change the boxColor to red. The moment the dragging on the widget is initiated, it is rebuild and defaults to the initial state (at least the Draggable feedback is). After the reordering that doesn't change and the widget is still in it's default state.
My plan here is to have a list of different custom widgets, where the User can modify their content and if they are not happy with the order, they can drag those widgets around and rearrange them.
My question is: How do I preserve the state of my widgets?
I'm thinking of creating a class for each widget with all state relevant variables and use that to build my widgets but that seems very bloated and not really in the mind of flutter. Isn't that supposed to be the role of the state of the StatefulWidget?
EDIT:
So I solved my problem by creating an additional class for my widget state with ChangeNotifier and then moving all my variables that I want to keep track of into this class. So I basically now have two lists, one for my widgets in the reorderable list and one for their states. I still think that this is kinda scuffed. If a widget in my list has additional children of its own, I would need to create separate state classes for each of them that need it and save them somewhere. This can get very messy, very quickly.
I'm trying to understand the class Navigator.
A Navigator Route seem to always return a new widget but what if I want to manage the TabBar and TabBarView so that each Tab when tapped or swipe on, will be pushed to the Navigator stack, I don't find a what to do that.
On a more general case, can I react to a route change without creating a new widget but instead taking another action like scrolling to a specific item in a listView?
I've tried recreating the entire app structure every time but doing this way I don't have the nice default animation and, also, doesn't seem a good approach to me.
You can use WillPopScope widget and its onWillPop to catch the back button pressure and handle it yourself. Find more info here https://docs.flutter.io/flutter/widgets/WillPopScope-class.html
On a more general case, can I react to a route change without creating
a new widget but instead taking another action like scrolling to a
specific item in a listView?
This looks more specific rather than general. However, you do need to set a ScrollController in your ListView and let it scroll the list for you to the desired point. A simple example function returning to top:
class MyFancyClass extends StatelessWidget{
...
ScrollController _scrollController;
...
#override
Widget build(BuildContext Context){
return ....
new ListView(
...
controller: _scrollController,
...
}
void _toTop() {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.ease,
);
}
Check https://docs.flutter.io/flutter/widgets/ScrollController-class.html for more details and behaviors. In case you want the back button to bring you to the top:
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onTop,
child:
...
),
);
}
Concerning the tab behavior I suggest you to read https://docs.flutter.io/flutter/material/TabController-class.html to better understand how to implement what you have in your mind.