1. Keep build
the method pure
build
Methods must be pure/free of anything not needed. This is because there are some external factors that can trigger a new widget build, here are some examples:
-
Route pop/push
-
Screen resizing, usually due to keyboard display or screen orientation changes
-
The parent widget recreates its child widget
-
Widget dependent
InheritedWidget
(Class. of(context)
mode) changes
DON’T:
Widget build(BuildContext context) {
return FutureBuilder(
future: httpCall(),
builder: (context, snapshot) {
// create some layout here
},
);
}
DO:
class Example extends StatefulWidget {
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
void initState() {
future = repository.httpCall();
super.initState();
}
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
}
2. Understand the concept of Flutter layout constraints
There is a rule of thumb for Flutter layout that every Flutter app developer needs to know: Constraint down, size up, parent element set position .
-
Widgets have constraints from their parent components. Known constraints are a set of four doubles: minimum and maximum width, minimum and maximum height.
-
Next, the widget will iterate over its own sublist. The widget instructs its children one by one what the constraints are (which may be different for each child), and then asks each child what size it wants.
-
Next, the widget positions its child widgets in turn (horizontal x-axis, vertical y-axis). The widget then informs its parent of its own size (within the original constraints, of course).
In Flutter, all widgets provide themselves based on their parent component or their box constraints. A widget's size must be within the constraints set by its parent component.
3. Use operators to reduce the number of lines of code executed
- Use the cascade operator
If we want to perform a sequence of operations on the same object, then we should choose ..
the operator:
DON’T:
var path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
path.close();
DO:
var path = Path()
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, 0)
..close();
- Use the set spread operator
The spread operator can be used when the existing items are already stored in another collection, the spread collection syntax makes the code simpler.
DON’T:
var y = [4,5,6];
var x = [1,2];
x.addAll(y);
DO:
var y = [4,5,6];
var x = [1,2,...y];
- Using the Null-safe (
??
) and Null-aware (?.
) operators
??
The (if null
) and ?.
( null
aware) operators should always be pursued first in code , not as null
checks in conditional expressions.
DON’T:
v = a == null ? b : a;
v = a == null ? null : a.b;
DO:
v = a ?? b;
v = a?.b;
- Use the "
is
" operator whenever possible instead of the "as
" operator
Normally, the cast operator throws an exception if a cast cannot be made. To prevent an exception from being thrown, " is
" can be used.
DON’T:
(item as Animal).name = 'Lion';
DO:
if (item is Animal) item.name = 'Lion';
- Initialize growable collections using literals
Good:
var points = [];
var addresses = {
};
Bad:
var points = List();
var addresses = Map();
With generics:
Good:
var points =<Point>[];
var addresses = <String, Address>{
};
Bad:
var points = List<Point>();
var addresses = Map<String, Address>();
4. Use only when neededStream
While streams are very powerful, if we use them, there is a great responsibility on our shoulders in order to utilize this resource effectively.
Using a stream with poor performance may result in more memory and CPU usage . Not only that, but you can cause memory leaks if you forget to close the stream .
So instead of using Stream in this case, use something less memory consuming like ChangeNotifier for responsive UI . For more advanced functionality, we can use the Bloc library, which puts more focus on using resources in an efficient manner and provides a simple interface to build responsive UIs.
Streams are effectively flushed whenever they are no longer used. The problem here is that if you just delete the variable, it's not enough to ensure it's not used. It can still run in the background.
You need to call Sink.close()
it so that it stops the related StreamController
to ensure that the resource can be freed by the GC later . For this, StatefulWidget.dispose
the processing method must be used:
abstract class MyBloc {
Sink foo;
Sink bar;
}
class MyWiget extends StatefulWidget {
_MyWigetState createState() => _MyWigetState();
}
class _MyWigetState extends State<MyWiget> {
MyBloc bloc;
void dispose() {
bloc.bar.close();
bloc.foo.close();
super.dispose();
}
Widget build(BuildContext context) {
// ...
}
}
5. Write tests for key functions
There will always be occasional situations where you rely on manual testing, and having an automated set of tests can help you save a lot of time and effort. Since Flutter primarily targets multiple platforms, testing each feature after each change is time-consuming and requires a lot of repetitive work.
Let's face it, 100% code coverage for testing is always the best option, however, depending on the time and budget available, this is not always possible. Nonetheless, it is still necessary to have at least tests to cover the critical functionality of the application.
Unit testing and Widget testing is the most important choice from the beginning, compared to integration testing, it is not boring at all.
6. Using raw
strings
Raw strings can be used to escape only backslashes and dollar signs.
DON’T:
var s = 'This is demo string \ and $';
DO:
var s = r'This is demo string and $';
7. Use relative imports instead of absolute imports
When using both relative and absolute imports, it can become confusing when importing the same class from two different ways. To avoid this, we should lib/
use relative paths in folders.
DON’T:
import 'package:myapp/themes/style.dart';
DO:
import '../../themes/style.dart';
8. Use SizedBox
insteadContainer
If you have multiple use cases you need to use placeholders. Here's an ideal example:
return _isNotLoaded ? Container() : YourAppropriateWidget();
Container
is a great widget that you will use extensively in Flutter. Container()
Extends to fit the constraints given by the parent class and is not const
a constructor.
Therefore, when we have to implement placeholders, we should use SizedBox
instead of Container
.
DON’T:
Widget showUI() {
return Column(
children: [loaded ? const ActualUI() : Container()],
);
}
DO:
Widget showUI() {
return Column(
children: [loaded ? const ActualUI() : const SizedBox()],
);
}
Better:
Widget showUI() {
return Column(
children: [loaded ? const ActualUI() : const SizedBox.shrink()],
);
}
The benefits of doing this:
SizedBox
There is aconst
constructor,Container
which can result in more efficient code compared toSizedBox
Container
is a lighter object than the one to instantiate .SizedBox.shrink()
Sets the width and height to0
, initialized to by defaultnull
.
You can also useSizedBox
instead ofSizedBox.shrink
, but using the word "shrink" makes it clear that this widget will take up minimal (or zero) space on the screen.Container
If the widget has no children, no height, no width, no join constraints, and no alignment, but the parent provides bounded constraints,Container
it will expand to fit the constraints provided by the parent .
9. Use log
insteadprint
print()
and debugPrint()
are always used to log in to the console. If you're using print()
and you're getting too much output at once, Android will drop some log lines every now and then.
To avoid this situation again, use debugPrint()
. Use if your log data has enough data dart: developer log()
. This enables you to add more granularity and information to your log output.
DON’T:
print('data: $data');
DO:
log('data: $data');
10. Only Debug
used in modeprint
Make sure print
and log
statements are only used in the application's Debug
mode.
You can use kDebugMode
detection Debug
or Release
pattern
kReleaseMode
, inRelease
the pattern istrue
kProfileMode
, inProfile
the pattern istrue
import "dart:developer";
import 'package:flutter/foundation.dart';
testPrint() {
if (kDebugMode) {
log("I am running in Debug Mode");
}
}
11. The correct choice to use the ternary operator
- Use the ternary operator in the single-line case
String alert = isReturningCustomer ? 'Welcome back!' : 'Welcome, please sign up.';
if
Situations when an alternative ternary operator should be used
Widget getText(BuildContext context) {
return Row(
children:
[
Text("Hello"),
if (Platform.isAndroid) Text("Android") (这里不应该使用三元运算符)
]
);
}
DON’T:
Widget showUI() {
return Row(
children:[
const Text("Hello Flutter"),
Platform.isIOS ? const Text("iPhone") : const SizedBox(),
],
);
}
DO:
Widget showUI() {
return Row(
children:[
const Text("Hello Flutter"),
if (Platform.isI0S) const Text("iPhone"),
],
);
}
Also:
Widget showUI() {
return Row(
children:[
const Text("Hello Flutter"),
if (Platform.isI0S) ...[
const Text("iPhone"),
const Text('MacBook'),
]
],
);
}
12. Always try to useconst Widget
When setState
called, if Widget
will not change, we should define it as a constant. It will prevent Widget
rebuilds of the , thus improving performance.
Also, Widget
using const
a constructor for a class reduces the work required by the garbage collector. This might seem like a small performance optimization at first, but when the application is large enough or has a view that gets rebuilt frequently, it can actually yield a big benefit. const
Declarations are also better suited for hot reloading.
DON’T:
SizedBox(height: Dimens.space_normal)
DO:
const SizedBox(height: Dimens.space_normal)
Also, we should ignore unnecessary const
keywords. Take a look at the code below:
const Container(
width: 100,
child: const Text('Hello World')
);
We don't need to Text
use for const
because const
it's already applied to the parent component.
Dart const
provides the following linter rules for :
- prefer_const_constructors
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- Unnecessary_const
13. Always display the type of the specified member variable
The type of a member is always highlighted when its value type is known. Don't use it when you don't need it var
. Since var
it is a dynamic type, it requires more space and time to parse.
DON’T:
var item = 10;
final car = Car();
const timeOut = 2000;
DO:
int item = 10;
final Car bar = Car();
String name = 'john';
const int timeOut = 20;
14. Some points to keep in mind
-
Never forget to wrap the root widget in a safe area.
-
Whenever possible, make sure to use
final/const
class variables. -
Try not to use unnecessarily commented code.
-
Create private variables and methods whenever possible.
-
Build different classes for colors, text styles, dimensions, constant strings, durations, and more.
-
Use constants to represent API Keys.
-
await
Try not to use keywords in blocks -
Try not to use global variables and functions. They must be
Class
closely linked with each other. -
Check the Dart analysis and follow its recommendations
-
Check for underscores, typo suggestions or optimization tips
-
_
Use (underscore) if the value is not used in the code block .
DON’T:
someFuture.then((DATA_TYPE VARIABLE) => someFunc());
DO:
someFuture.then((_) => someFunc());
- Magic numbers should always be named appropriately for human readability.
DON’T:
SvgPicture.asset(
Images.frameWhite,
height: 13.0,
width: 13.0,
);
DO:
final _frameIconSize = 13.0;
SvgPicture.asset(
Images.frameWhite,
height: _frameIconSize,
width: _frameIconSize,
);
15. Avoid functional components
DON’T:
class HomePage extends StatelessWidget {
const HomePage({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(30),
child: functionWidget(child: const Text('Hello')),
),
);
}
Widget functionWidget({
required Widget child}) {
return Container(child: child);
}
}
DO:
class HomePage extends StatelessWidget {
const HomePage({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const Scaffold(
body: Padding(
padding: EdgeInsets.all(30),
child: ClassWidget(child: Text('Hello')),
),
);
}
}
class ClassWidget extends StatelessWidget {
final Widget child;
const ClassWidget({
Key? key, required this.child}) : super(key: key);
Widget build(BuildContext context) {
return Container(child: child);
}
}
The benefits of doing this:
- By using functions to split the Widget tree into multiple Widgets, you expose yourself to bugs and miss out on some performance optimizations.
- Using functions doesn't guarantee that you will have bugs, but using classes can guarantee that you won't face these problems.
List
16. Using the Widget's Item Extent
property in a long list
ItemExtent
This can significantly improve performance if you want to jump to a specific index by clicking a button or otherwise .
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
controller: _scrollController,
itemExtent: 600,
children: List.generate(10000, (index) => Text('index: $index')),
),
)
}
The benefits of doing this:
- Specifying
itemExtent
is more efficient than letting children determine their ranges, because the scrolling system already knows the children's ranges, which saves time and effort.
17. Use in a more readable wayasync/await
DON’T:
Future<int> getUsersCount() async {
return getUsers().then((users) {
return users.length;
}).catchError((e) {
return 0;
});
}
DO:
Future<int> getUsersCount() async {
try {
var users = await getActiveUser();
return users.length;
} catch (e) {
return 0;
}
}
18. ListView.builder
Build a list with the same view using
Listview.builder
Created lists generate row views only as needed.Listview.builder
Repurpose an off-screen row view for a user-visible row view.- By default
ListViews
rows are not reused and the list is created all at once, which can immediately cause performance issues if the list is too large.
19. Split Widgets
- Splitting larger ones
Widget
into smallerWidget
components helps reuse and improves performance. - Don't use functions for larger
Widget
returnsWidget
, it can lead to unnecessary calls to functions, which are expensive. - All derived ones will be regenerated when
State
called on . So, splitting it into smaller components allows you to only call the parts of the subtree that actually need to change the UI.setState()
Widget
Widget
Widget
setState()
20. Use in a separate fileColors
Try putting all the colors of your app in one class, and also strings if you're not using localizations, so whenever you want to add localizations, you can find them all in one place.
class AppColor {
static const Color red = Color(ØxFFFF0000);
static const Color green = Color(0xFF4CAF50);
static const Color errorRed = Color(0xFFFF6E6E);
}
21. Using Dart Code Metrics
One of the best practices for Flutter code structure is to use Dart Code Metrics . This is an ideal way to improve the overall quality of your Flutter app.
DCM (Dart Code Metrics) is a static code analysis tool that helps developers monitor and temporarily adjust the overall quality of Flutter code. The various metrics that developers can view include many parameters, executable lines of code, and more.
Some of the Flutter best practices mentioned in the official Dart Code Metrics documentation include:
- Avoid using
Border.all
constructors - avoid unnecessary
setState()
- avoid returning
widgets
- It is better to extract the callback callback
- Preferably only one per file
Widget
- It is better to use constant
const
decorationborder-radius
22. Use Fittedbox
Flutter to implement responsive layout
To implement responsive design in Flutter, we can utilize FittedBox
components.
FittedBox
is a Flutter Widget that limits the size growth of sub-Widgets after a certain limit. It rescales child components based on available sizes.
Adaptation principle:
-
FittedBox
When laying out subcomponents , the constraints passed by their parent components are ignored , and subcomponents can be allowed to be infinitely large , that is,FittedBox
the constraints passed to subcomponents are (0 <= width <= double.infinity, 0 <= height <= double.infinity
). -
FittedBox
The real size of the subcomponent can be obtained after the layout of the subcomponent is completed . -
FittedBox
Knowing the real size of the child component and the constraints of its parent component , thenFittedBox
you can use the specified adaptation method (BoxFit
specified in the enumeration) to make the child componentFittedBox
display in the specified way within the constraints of the parent component.
For example, if we create a container in which the text entered by the user will be displayed, if the user enters a very long text string, the container will exceed its allowed size. However, if we use FittedBox
a wrapper container, it will accommodate the text according to the available size of the container. If the text exceeds FittedBox
the container size set using, the text size will be reduced to fit it in the container.
DON’T:
Padding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
child: Row(children: [Text('xx'*30)]), //文本长度超出 Row 的最大宽度会溢出
)
DO:
Padding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
child: FittedBox(
child: Row(children: [Text('xx'*30)]),
),
)
23. Flutter Security Practices
Security is an integral part of any mobile application, especially in this age of mobile-first technology. In order for many apps to function properly, they require many of the user's device permissions and sensitive information about their finances, preferences, and other factors.
It is the developer's responsibility to ensure that the application is sufficiently secure to protect such information. Flutter provides excellent security, here are the best Flutter security practices you can use:
- Code obfuscation
- Prevent background snapshots
Prevent background snapshots
Normally, when your app is running in the background, it automatically displays the app's last state in the task switcher or multitasking screen. This is useful when you want to see what your last activity was on a different app; however, in some cases you don't want to expose screen information in the task switcher. For example, you don't want your bank account details to show up on your app's screen in the background. You can use the secure_application package to protect your Flutter application from such issues.
24. Other Tips
-
1)
Row
andColumn
the layout setting the main axis alignment tospaceEvenly
will divide the free space evenly between, before and after each image:mainAxisAlignment: MainAxisAlignment.spaceEvenly
In practice, this is very useful for evenly spaced sub-controls in a row or column. -
2) Setting
Row
andColumn
of to , can make the children close togethermainAxisSize
MainAxisSize.min
, by default, the row or column will take up as much space as possible along its main axis. -
3)
Expanded
OrWrap
the component can solve the problem of interface overflow,Expanded
you can setflex
the ratio -
4) Each
Element
corresponds to oneRenderObject
, which we canElement.renderObject
obtain through .RenderObject
The main responsibilities are layout and drawing, all of whichRenderObject
will form a rendering tree Render Tree . -
5)
RenderObject
It is an object in the rendering tree, which has oneparent
and oneparentData
slot (slot). This slot is a reserved variable, mainly used to storechild
offset dataoffset
(of course there are others), this offset Used during the drawing phase. -
6) According to
layout()
the source code, it can be seen that it will be called only when issizedByParent
, but will be called every time the layout is made.true
performResize()
performLayout()
sizedByParent
It means whether the size of the node can be determined only byparent
passed to it , that is, the size of the node has nothing to do with its own properties and its child nodes. For example, if a control is always full of the size of , then it should return , at this time Its size is determined in and will not be modified in the subsequent method. In this case, it is only responsible for laying out child nodes.constraints
parent
sizedByParent
true
performResize()
performLayout()
performLayout()
-
7) Layout layout process The final call stack will become: layout() > performResize() / performLayout() > child.layout() > ... , so recursively complete the layout of the entire UI.
-
8) The drawing process will traverse its child nodes, and then call paintChild() to draw the child nodes , and at the same time pass the child node saved
ParentData
in the layoutoffset
stage plus its own offset as the second parameterpaintChild()
, and if the child node has children When creating a node,paintChild()
the method will also call the method of the child nodepaint()
, so that the drawing of the entire node tree is completed recursively, and the final call stack is: paint() > paintChild() > paint() … . -
9) isRepaintBoundary can improve the drawing performance. When
RenderObject
drawing is very frequent or complex, it can be specified by WidgetRepaintBoundary
, so that only redraw itself and no need to redraw it when drawing , so that the performance can be improved.isRepaintBoundary
true
parent
-
10) The widget in Flutter is rendered by the underlying RenderBox object. The rendering box is given constraints by its parent widget and resizes itself according to those constraints. Constraints are composed of four aspects: minimum width, maximum width, minimum height, and maximum height; size is composed of specific width and height.
-
11) In general, from the point of view of how constraints are handled, there are three types of rendering boxes as follows:
- as large as possible . Such as the rendering boxes of
Center
andListView
. - Same size as child widgets , such as
Transform
andOpacity
render boxes. - Render boxes of specific sizes , such as
Image
and .Text
When passing an unbounded (maximum width or maximum height of double.INFINITY) constraint to a box of type as large as possible will fail, and in debug mode, an exception will be thrown.
The most common cases where render boxes have unbounded constraints are when they are placed inside flex boxes (Row and Column) and scrollable areas (ListView and other ScrollView subclasses) .
- as large as possible . Such as the rendering boxes of
-
12) The Flex itself (Row and Column) behaves differently depending on whether it is bounded or unbounded in a given direction.
-
They are as large as possible in a given direction, subject to boundary constraints.
-
Under no bounds constraints, they attempt to adapt their child widgets to the given orientation . In this case, the properties
widget
of the child cannotflex
be set to0
any value other than (the default). This means that in the widget library, it cannot be used when aflex
box is nested inside anotherflex
box or inside a scrollable areaExpanded
. If you do, you'll get an exception.
In cross directions, such as width
Column
(vertical ) and height (horizontal ), they must not be unbounded , otherwise they won't be able to properly align their children .flex
Row
flex
widget
-
-
13)
Text
Setsoftwrap
totrue
, the text will automatically wrap at word boundaries after filling the column width. -
14) Flutter prefers composition over inheritance. Composition defines the "
has a
" relationship, and inheritance defines the "is a
" relationship. -
15) The widget should be immutable in refresh, but the state object
State
is mutable. -
16) A
StatefullWidget
keeps track of its own internal state through an associated state object.StatefullWidget
is "dumb",widget
it is completely destroyed when it is removed from the tree. -
17) In Flutter, it is rendered
widget
by its associatedRenderBox
object . These render boxes are responsible for telling the widget its actual physical size. These objects receive constraints from their parents, and use those constraints to determine their actual size. -
18)
Container
A component is a "convenience"widget
that provides a large number of properties that you might otherwise need from eachWidget
.