When dragging modal bottom sheets, the flutter application starts lagging if a lot of widgets live inside the sheet. This only occurs on the modal bottom sheet (showModalBottomSheet) and not on the normal one (showBottomSheet).
Below I attached a screenshot of the performance analysis, which shows, that all widgets inside the sheet are beeing constantly rebuilt while the user is dragging.
I wrote a little demo to compare the performance of the two types of sheets. Is there a way to prevent the rebuilding while dragging?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
/// This Widget is the main application widget.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "demo",
home: Scaffold(
body: MyButtons(),
),
);
}
}
class MyButtons extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (context) => BottomSheet(),
);
},
child: Text("show modal (laggy)"),
),
RaisedButton(
onPressed: () {
showBottomSheet<void>(
context: context,
builder: (context) => BottomSheet(),
);
},
child: Text("show normal (not laggy)"),
),
],
),
);
}
}
class BottomSheet extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Wrap(
spacing: 8.0,
alignment: WrapAlignment.center,
children: List<Widget>.generate(
100,
(int index) {
return InputChip(
label: Text("test"),
);
},
),
);
}
}
I have created this PR to fix this performance issue. The problem was that the AnimatedContainer from the ModalBottomSheet was not using the child property and therefore it was forcing to call builder method many times while animation is running instead of using the already built child widget.
just update flutter to latest version. kudos to Enol Casielles Martinez
Related
I'm new to flutter, and I'm trying to divide the screen into three parts, here's my attempt:
class Homepage extends Statelesswidget {
const HomePage({Key? key}): super (key: key);
#override
Widget build(BuildContext context) {
return Row(children: [
const Expanded(
flex: 1,
child: Scaffold(
body: Center(child: Text("Left")),
), // Scaffold
), // Expanded
Expanded(
flex: 2,
child: Scaffold(
appBar: AppBar(),
body: const Center(child: Text("Middle")),
), // Scaffold
), // Expanded
const Expanded(
flex: 1,
child: Scaffold(
body: Center(child: Text("Right")),
), // Scaffold
) // Expanded
]);
}
}
and main.dart looks like:
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
Here's the result:
I got these vertical lines out of nowhere, how can I get rid of them?
Welcome to Flutter 💙
These lines are the boarders of the Scaffold widget which you are using inside each Expanded widget in the row.
Now intead of looking for a way to remove these border lines, I suggest you to learn more about the Scaffold widget in Flutter
Since you are new to Flutter, you have to be aware with the goal of using Scaffold widget, which is stated in the official docs as the following:
Implements the basic material design visual layout structure.
This class provides APIs for showing drawers and bottom sheets.
So in other words, we use the Scaffold Widget as a basic canvas for the screen with some properties to implement popular layout designs, such as AppBar, BottomNavigationBar, FloatingActionButton, etc...
Then, we use it once in the screen to set the basic layout, and does not required to be used multiple times for each sub part of the screen
Then your HomePage code should be something like:
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: [
const Expanded(
flex: 1,
child: Center(child: Text("Left")),
), // Expanded
Expanded(
flex: 2,
child: Column(
children: [
AppBar(),
const Expanded(
child: Center(child: Text("Middle")),
),
],
),
),
const Expanded(
flex: 1,
child: Center(child: Text("Right")),
)
]),
);
}
}
I'm developing my first app with flutter. At some point I was wondering :Am I developping the UX part correctly ? Meaning am I using the proper widget, is there any better way to do that etc.. I find out about Flutter Performance on Intellj Idea and I saw that most of the pages I developed are red...
FYI : The code I created for a simple page
Flutter inspector result => radio-btn-aligned
import 'package:flutter/material.dart';
import 'package:testapp/my_theme.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: MyAppTheme(ctx: context).defaultTheme,
home: new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
),
body: AddDailyTaskPage()),
);
}
}
enum Options { goal, category }
class AddDailyTaskPage extends StatefulWidget {
#override
_AddDailyTaskPageState createState() => new _AddDailyTaskPageState();
}
class _AddDailyTaskPageState extends State<AddDailyTaskPage> {
Options _options = Options.goal;
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Container(
child: Text("Task Description"),
),
Container(child: Row(
children: [
Expanded(
child: Row(
children: [
Radio<Options>(
value: Options.goal,
groupValue: _options,
onChanged: (Options value) {
setState(() {
_options = value;
});
},
),
Text(
'Goal',
style: new TextStyle(fontSize: 16.0),
)
]
)
),
Expanded(
child: Row(
children: [
Container(
child: Radio(
value: Options.category,
groupValue: _options,
onChanged: (Options value) {
setState(() {
_options = value;
});
},
),
),
Container(
child: Text(
'Category',
style: new TextStyle(
fontSize: 16.0,
),
),
)
],
)
)
],
),)
// Container(
// child: TextField(
// maxLines: 10,
// decoration: InputDecoration(
// // suffixIcon:
// focusedBorder: OutlineInputBorder(
// borderSide: BorderSide(color: Colors.grey, width: 5.0),
// ),
// enabledBorder: OutlineInputBorder(
// borderSide: BorderSide(color: Colors.black, width: 5.0),
// ),
// hintText: 'Description task',
// ),
// ),
// )
],
),
);
}
}
To see the difference, I checked the sample code provided while creating a flutter Project
FYI :
flutter performance result : auto-increment
As we can see on the previous pic, it doesn't seems optimised.. :/
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Few questions :
1/ Does anyone has / knows a good flutter repo with ton of examples based on performance ?
2/ What is your standard in term of perf ? I mean on my virtual device, the UX seems fluid but if the Flutter Perf is "red", then I'm guessing I'm doing something wrong and there is a better way to do ?
3/ Do you guys knows a website / forum / someone who's willing to do a core review once a week to help me implementing good pattern in my flutter application ?
4/ What is wrong with my current design ? What is wrong with the default design ? Why the performance are doesn't seems good ? I started to read the official documentation for the perf, but how could I know if the UX itself have good perf or not ? Actually by testing some stuff, I find out that putting everything in Container / Row or Column widget, improved a lot the performance but even with this widgets, it's not enough :/
Any advice ?
Thanks for your help
You use a StatefulWidget for the whole page. That means that when you call setState() the whole page gets rebuild.
One example is:
Radio<Options>(
value: Options.goal,
groupValue: _options,
onChanged: (Options value) {
setState(() {
_options = value;
});
},
),
There's no need to rebuild your whole page when you set an option. If you extract that code out into it's own StatefulWidget, only this portion gets rebuild.
You can click on "Track Widget rebuilds" to see what Widgets do rebuild in your app and then think about whether those Widget actually should rebuild.
Once you do smaller Widgets you get the problem that events in one Widget are supposed to influence other Widgets. That's when state management solutions come into play. The weather example of the Bloc package shows a good structure of an app that you can copy.
Is there any way to change the default animation when navigating to/from a page in Flutter?
You can use PageRouteBuilder.
The following example shows FadeTransition when you navigate to second screen.
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => Page2(),
transitionDuration: Duration(seconds: 2),
transitionsBuilder: (_, a, __, c) => FadeTransition(opacity: a, child: c),
),
);
If you're using go_router:
GoRoute(
path: '/page2',
pageBuilder: (_, state) {
return CustomTransitionPage(
key: state.pageKey,
child: Page2(),
transitionDuration: Duration(seconds: 2),
transitionsBuilder: (_, a, __, c) => FadeTransition(opacity: a, child: c),
);
},
)
and then:
context.go('/page2');
You can subclass MaterialPageRouteand override buildTransitions.
Eg:
class MyCustomRoute<T> extends MaterialPageRoute<T> {
MyCustomRoute({ WidgetBuilder builder, RouteSettings settings })
: super(builder: builder, settings: settings);
#override
Widget buildTransitions(BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
if (settings.isInitialRoute)
return child;
// Fades between routes. (If you don't want any animation,
// just return child.)
return new FadeTransition(opacity: animation, child: child);
}
}
to use :
new RaisedButton(
child: new Text('Goto'),
onPressed: (){
Navigator.push(
context,
new MyCustomRoute(builder: (context) => new SecondPage()),
);
}),
Replace fade transition with your animation
You can achieve this by using CupertinoPageRoute.
Please check the below code.
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Transition Animation Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new FirstPage(),
);
}
}
class FirstPage extends StatefulWidget {
#override
_FirstPageState createState() => new _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('First Page'),
),
body: new Center(
child: new RaisedButton(
child: new Text('Goto Second Page'),
onPressed: () {
Navigator.of(context).push(new SecondPageRoute());
},
),
),
);
}
}
class SecondPageRoute extends CupertinoPageRoute {
SecondPageRoute()
: super(builder: (BuildContext context) => new SecondPage());
// OPTIONAL IF YOU WISH TO HAVE SOME EXTRA ANIMATION WHILE ROUTING
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return new FadeTransition(opacity: animation, child: new SecondPage());
}
}
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => new _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Second Page'),
),
body: new Center(
child: new Text('This is the second page'),
),
);
}
}
Some play-around with animation
// OPTIONAL IF YOU WISH TO HAVE SOME EXTRA ANIMATION WHILE ROUTING
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return new RotationTransition(
turns: animation,
child: new ScaleTransition(
scale: animation,
child: new FadeTransition(
opacity: animation,
child: new SecondPage(),
),
));
}
I have done this by providing my own builders with custom map in pageTransitionsTheme for the app level theme.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator Tile',
home: RandomWords(),
theme: new ThemeData(
primaryColor: Colors.white,
// Add the line below to get horizontal sliding transitions for routes.
pageTransitionsTheme: PageTransitionsTheme(builders: {TargetPlatform.android: CupertinoPageTransitionsBuilder(),}),
),
);
}
}
Of course, I didn't add a map entry for ios as I use only android for TargetPlatform.
You can also check out page_transition package from https://pub.dev/packages/page_transition. This package contains the following different transitions.
fade,
rightToLeft,
leftToRight,
upToDown,
downToUp,
scale (with alignment),
rotate (with alignment),
size (with alignment),
rightToLeftWithFade,
leftToRightWithFade
the simplest way I figured, is to use MaterialPageRoute normally just add: fullscreenDialog: true, inside MaterialPageRoute()
I would like to have my list items perform this animation (mp4) when tapped. I tried using AnimatedCrossFade but it requires its two children to be at the same level, e.g. the detail view cross-fades with the ListView not the tapped item. In fact it seems a Hero animation is the only one that can animate across widgets.
I'm having trouble using Hero. Should it wrap the list item? Does it matter if the Widget subtree is significantly different in the Hero source/destination? Also, can Hero animations be used with LocalHistoryRoutes or staggered animations?
Edit
It's now looking like what I need to do is use an Overlay, the hard part there is that I need to add the selected item to the overlay at the same position on screen where it was tapped, then the animation part would be easy. Possibly of use here is a target/follower pattern e.g. CompositedTransformTarget
You can just use Hero widget to make that kind of animation. Here's my example:
and the source code:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new FirstPage(title: 'Color Palette'),
);
}
}
class FirstPage extends StatefulWidget {
FirstPage({Key key, this.title}) : super(key: key);
final String title;
#override
_FirstPageState createState() => new _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final palette = [
{'#E53935': 0xFFE53935},
{'#D81B60': 0xFFD81B60},
{'#8E24AA': 0xFF8E24AA},
{'#5E35B1': 0xFF5E35B1},
{'#3949AB': 0xFF3949AB},
{'#1E88E5': 0xFF1E88E5},
{'#039BE5': 0xFF039BE5},
{'#00ACC1': 0xFF00ACC1},
{'#00897B': 0xFF00897B},
{'#43A047': 0xFF43A047},
{'#7CB342': 0xFF7CB342},
{'#C0CA33': 0xFFC0CA33},
];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Container(
child: new ListView.builder(
itemCount: palette.length,
itemBuilder: (context, index) => new Hero(
tag: palette[index].keys.first,
child: new GestureDetector(
onTap: () {
Navigator
.of(context)
.push(new ColorPageRoute(palette[index]));
},
child: new Container(
height: 64.0,
width: double.infinity,
color: new Color(palette[index].values.first),
child: new Center(
child: new Hero(
tag: 'text-${palette[index].keys.first}',
child: new Text(
palette[index].keys.first,
style: Theme.of(context).textTheme.title.copyWith(
color: Colors.white,
),
),
),
),
),
),
)),
),
);
}
}
class SecondPage extends StatelessWidget {
final Map<String, int> color;
SecondPage({this.color});
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Color'),
),
body: new Hero(
tag: color.keys.first,
child: new Container(
color: new Color(color.values.first),
child: new Center(
child: new Hero(
tag: 'text-${color.keys.first}',
child: new Text(
color.keys.first,
style:
Theme.of(context).textTheme.title.copyWith(color: Colors.white),
),
),
),
),
),
);
}
}
class ColorPageRoute extends MaterialPageRoute {
ColorPageRoute(Map<String, int> color)
: super(
builder: (context) => new SecondPage(
color: color,
));
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: animation, child: child);
}
}
Someone wrote an amazing dart-package for just this purpose: https://pub.dev/packages/morpheus#-readme-tab-
All you then need to do is use the MorpheusPageRoute and the package handles the rest.
...
Navigator.push(
context,
MorpheusPageRoute(
builder: (context) => MyWidget(title: title),
),
);
...
I'd just cheat and wrap the whole thing in a Stack - bottom layer would be a page with the AppBar, and the top layer would be transparent until painted on.
onTap, duplicate ListTile onto the top surface, and then a Hero animation would fill the full screen. It's not very elegant, but the framework doesn't (yet) provide for covering the AppBar easily, so having a canvas ready to be painted on for other tricky animations might be resourceful.
I'm unable to comment or edit Lucas' post (new account) but you also need to provide the parentKey of the widget where the animation is to begin:
final widgetKey = GlobalKey();
...
ListTile(
key: widgetKey,
title: Text('My ListItem'),
onTap: () => Navigator.push(
context,
MorpheusPageRoute(
builder: (context) => MyNewPage(),
parentKey: widgetKey,
),
),
),
https://pub.dev/packages/morpheus
After adding firebase_admob plugin and getting it up and running I noticed it overlays the fab and navigation drawer. I've fixed the fab using persistentFooterButtons but I can't seem to find a workaround for the navigation-drawer. Any help is much appreciated.
Find below a sample implementation, to recreate the issue in flutter:
import 'package:flutter/material.dart';
import 'package:firebase_admob/firebase_admob.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Firebase AdMob',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'AdMob Test App'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
BannerAd myBanner;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
void initState() {
super.initState();
myBanner = new BannerAd(
// Replace the testAdUnitId with an ad unit id from the AdMob dash.
// https://developers.google.com/admob/android/test-ads
// https://developers.google.com/admob/ios/test-ads
adUnitId: BannerAd.testAdUnitId,
size: AdSize.smartBanner,
targetingInfo: new MobileAdTargetingInfo(
// gender: MobileAdGender.unknown
),
listener: (MobileAdEvent event) {
print("BannerAd event is $event");
},
);
myBanner..load()..show(
// Banner Position
anchorType: AnchorType.bottom,
);
}
#override
void dispose() {
myBanner?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
drawer: new Drawer(),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('You have pushed the button this many times:'),
new Text('$_counter', style: Theme.of(context).textTheme.display1),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
I'm a little late to this but had same problem.
My nav drawer lives in a scrollable container with a fixed height so that it stops above the add and is scrollable. May not be perfect answer but works for me.
I had the same problem and my solution was the same Added by #moehagene. I added an empty item to the bottom of the drawer with the height of the Ad, so the drawer becomes scrollable when there is not enough space and the Ad is showing. I think this is reasonable. Code below:
return Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: Column(
children: <Widget>[
Expanded(
child: Container(
child: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: <Widget>[
_Item1,
_Item2,
_Item3,
_Item4,
model.isShowingAds ? _emptySpaceItem : null,
],
),
),
),
],
),
);