Reputation: 25
I want to know why that Flutter code is below 60 fps in some devices (Redmi Go, in that case, profiling mode, showing red bars in UI graph). Is there any way i could optimize it? I began to study Flutter yesterday so i want some tips to help me understand stateful widgets and widgets rendering hierarchy in general.
Thanks in advance.
File main.dart
import 'package:project/ui/home.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MaterialApp(
home: BillSplitter(),
));
File home.dart
import 'package:flutter/material.dart';
class BillSplitter extends StatefulWidget {
@override
_BillSplitterState createState() => _BillSplitterState();
}
class _BillSplitterState extends State<BillSplitter> {
int _tipPercentage = 0;
int _personCounter = 1;
double _billAmount = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"APP Teste 1.0",
style: TextStyle(fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: Colors.purple.withOpacity(0.5),
),
body: Container(
margin: EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.1),
alignment: Alignment.center,
color: Colors.white,
child: ListView(
scrollDirection: Axis.vertical,
padding: EdgeInsets.all(20.5),
children: <Widget>[
Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.purple.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.0)),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"Total por pessoa",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.normal,
fontSize: 17.0),
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
"R\$ ${_calculateTotalPerPerson(_billAmount, _personCounter, _tipPercentage)}",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 35.0),
),
)
],
),
),
),
Container(
margin: EdgeInsets.only(top: 20.0),
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.all(
color: Colors.blueGrey.shade100,
style: BorderStyle.solid),
borderRadius: BorderRadius.circular(12.0)),
child: Column(
children: <Widget>[
TextField(
keyboardType:
TextInputType.numberWithOptions(decimal: true),
style: TextStyle(color: Colors.purple),
decoration: InputDecoration(
prefixText: "Total da Conta: R\$ ",
prefixIcon: Icon(Icons.attach_money)),
onChanged: (String value) {
try {
_billAmount = double.parse(value);
} catch (e) {
_billAmount = 0.0;
}
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"Dividir",
style: TextStyle(color: Colors.grey.shade700),
),
Row(
children: <Widget>[
InkWell(
onTap: () {
setState(() {
if (_personCounter > 1) {
_personCounter--;
}
});
},
child: Container(
width: 40.0,
height: 40.0,
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7.0),
color: Colors.purpleAccent.withOpacity(0.1)),
child: Center(
child: Text(
"-",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
)),
),
),
Text(
"$_personCounter",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
),
InkWell(
onTap: () {
setState(() {
_personCounter++;
});
},
child: Container(
width: 40.0,
height: 40.0,
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7.0),
color: Colors.purpleAccent.withOpacity(0.1)),
child: Center(
child: Text(
"+",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
)),
),
),
],
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"Gorjeta",
style: TextStyle(color: Colors.grey.shade700),
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Text(
"R\$ ${(_calculateTotalTip(_billAmount, _personCounter, _tipPercentage)).toStringAsFixed(2)}",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
),
)
],
),
Column(
children: <Widget>[
Text(
"$_tipPercentage%",
style: TextStyle(
color: Colors.purple,
fontSize: 17.0,
fontWeight: FontWeight.bold),
),
Slider(
activeColor: Colors.purple,
inactiveColor: Colors.grey,
min: 0,
max: 100,
divisions: 10,
value: _tipPercentage.toDouble(),
onChanged: (double value) {
setState(() {
_tipPercentage = value.round();
});
})
],
)
],
),
)
],
),
),
);
}
_calculateTotalPerPerson(double billAmount, int splitBy, int tipPercentage) {
double totalPerPerson =
(billAmount + _calculateTotalTip(billAmount, splitBy, tipPercentage)) /
splitBy;
return totalPerPerson.toStringAsFixed(2);
}
_calculateTotalTip(double billAmount, int splitBy, int tipPercentage) {
double totalTip = 0.0;
if (billAmount < 0 || billAmount.toString().isEmpty || billAmount == null) {
} else {
totalTip = (billAmount * tipPercentage) / 100;
}
return totalTip;
}
}
Upvotes: 2
Views: 5335
Reputation: 91
You are using a single large build function, you need to split it into different widgets, because when the data state changes and you call setState()
, all descendant widgets will rebuild, so avoid using a very huge nested build function which will be very costly.
Also the performance of the application is affected by debugging, generating a release application will be more helpful to decide.
run this command to build your application
flutter build apk
.
you can also check Build and release an Android app or Build and release an iOS app
You can use Flutter profiling tools to determine the performance and determine performance problems in your application, you can find more useful details in the following link about Flutter performance profiling.
also check this link for more information and details about performance best practices.
Update
According to Performance best practices
Avoid overly large single Widgets with a large build() function. Split them into different Widgets based on encapsulation but also on how they change: When setState() is called on a State, all descendent widgets will rebuild. Therefore, localize the setState() call to the part of the subtree whose UI actually needs to change. Avoid calling setState() high up in the tree if the change is contained to a small part of the tree.
and according to the docs
Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
So it's way much better to divide your code to small widgets to avoid large nested widgets in a long build function, and I recommend reading about State management, it's very useful for managing data around your application and improves readability and maintainability for your code, which will eventually lead to a better performance.
Here i'm providing an example about widget decoupling using the code provided in this question, you need to read the comments in the following code to understand how I divided the application widgets, and I used flutter performance and frames per second didn't fall under 30 in debug mode, while the code you provided was dropping to below 15 frames per second.
You can compare both codes and see the difference in performance, and don't hesitate to ask me about the code for clarification.
File home.dart
import 'package:flutter/material.dart';
/*
I created this class for a better data management around the application
but this design is not recommended
*/
class Bill {
static int _tipPercentage = 0;
static int _personCounter = 1;
static double _billAmount = 0.0;
static _calculateTotalPerPerson(
double billAmount, int splitBy, int tipPercentage) {
double totalPerPerson =
(billAmount + _calculateTotalTip(billAmount, splitBy, tipPercentage)) /
splitBy;
return totalPerPerson.toStringAsFixed(2);
}
static _calculateTotalTip(double billAmount, int splitBy, int tipPercentage) {
double totalTip = 0.0;
if (billAmount < 0 || billAmount.toString().isEmpty || billAmount == null) {
} else {
totalTip = (billAmount * tipPercentage) / 100;
}
return totalTip;
}
}
// I converted the main widget to a stateless widget
// and for a better data management I highly recommend searching about
// state management
class BillSplitter extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"APP Teste 1.0",
style: TextStyle(fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: Colors.purple.withOpacity(0.5),
),
body: Container(
margin: EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.1),
alignment: Alignment.center,
color: Colors.white,
/*
Inside this ListView a lot of children widgets which can be converted
to smaller widgets in different classes based on the state of the widget
either it's stateless or stateful
*/
child: ListView(
scrollDirection: Axis.vertical,
padding: EdgeInsets.all(20.5),
children: <Widget>[
CurrentBillContainer(), //Check this widget class down below
BillCalculator(),
],
),
),
);
}
}
/*
This container is for the upper pink box which holds the bill value
and viewed in a different widget with a different build function
which will enhance the build() function time
*/
class CurrentBillContainer extends StatefulWidget {
@override
_CurrentBillContainerState createState() => _CurrentBillContainerState();
}
class _CurrentBillContainerState extends State<CurrentBillContainer> {
@override
Widget build(BuildContext context) {
return Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.purple.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.0)),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"Total por pessoa",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.normal,
fontSize: 17.0),
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
"R\$ ${Bill._calculateTotalPerPerson(Bill._billAmount, Bill._personCounter, Bill._tipPercentage)}",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 35.0),
),
)
],
),
),
);
}
}
class BillCalculator extends StatefulWidget {
@override
_BillCalculatorState createState() => _BillCalculatorState();
}
class _BillCalculatorState extends State<BillCalculator> {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 20.0),
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.all(
color: Colors.blueGrey.shade100, style: BorderStyle.solid),
borderRadius: BorderRadius.circular(12.0)),
child: Column(
children: <Widget>[
TotalBillTextField(),
DividirRow(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"Gorjeta",
style: TextStyle(color: Colors.grey.shade700),
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Text(
"R\$ ${(Bill._calculateTotalTip(Bill._billAmount, Bill._personCounter, Bill._tipPercentage)).toStringAsFixed(2)}",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
),
)
],
),
Column(
children: <Widget>[
Text(
"${Bill._tipPercentage}%",
style: TextStyle(
color: Colors.purple,
fontSize: 17.0,
fontWeight: FontWeight.bold),
),
Slider(
activeColor: Colors.purple,
inactiveColor: Colors.grey,
min: 0,
max: 100,
divisions: 10,
value: Bill._tipPercentage.toDouble(),
onChanged: (double value) {
setState(() {
Bill._tipPercentage = value.round();
});
})
],
)
],
),
);
}
}
/*
Take this TextField as an example you can create a stateless widget for it
and place it inside the column of BillCalculator class
and you can apply the same concept all over the application,
and divide widgets to small classes and inside sub folders to reduce the size
of the build function and optimize the performance and make it easier to
maintain and add a new features in your application
*/
class TotalBillTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextField(
keyboardType: TextInputType.numberWithOptions(decimal: true),
style: TextStyle(color: Colors.purple),
decoration: InputDecoration(
prefixText: "Total da Conta: R\$ ",
prefixIcon: Icon(Icons.attach_money)),
onChanged: (String value) {
try {
Bill._billAmount = double.parse(value);
} catch (e) {
Bill._billAmount = 0.0;
}
},
);
}
}
/*
This row has to be a Stateful widget because you are using
setState() function, you can apply the same method to all of the widgets
*/
class DividirRow extends StatefulWidget {
@override
_DividirRowState createState() => _DividirRowState();
}
class _DividirRowState extends State<DividirRow> {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"Dividir",
style: TextStyle(color: Colors.grey.shade700),
),
Row(
children: <Widget>[
InkWell(
onTap: () {
setState(() {
if (Bill._personCounter > 1) {
Bill._personCounter--;
}
});
},
child: Container(
width: 40.0,
height: 40.0,
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7.0),
color: Colors.purpleAccent.withOpacity(0.1)),
child: Center(
child: Text(
"-",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
)),
),
),
Text(
"${Bill._personCounter}",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
),
InkWell(
onTap: () {
setState(() {
Bill._personCounter++;
});
},
child: Container(
width: 40.0,
height: 40.0,
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7.0),
color: Colors.purpleAccent.withOpacity(0.1)),
child: Center(
child: Text(
"+",
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 17.0),
)),
),
),
],
),
],
);
}
}
Upvotes: 6