Flutter notes | Best Practice Tips for Flutter

1. Keep buildthe method pure

buildMethods 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 ?.( nullaware) operators should always be pursued first in code , not as nullchecks 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 StreamControllerto ensure that the resource can be freed by the GC later . For this, StatefulWidget.disposethe 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 rawstrings

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 SizedBoxinsteadContainer

If you have multiple use cases you need to use placeholders. Here's an ideal example:

return _isNotLoaded ? Container() : YourAppropriateWidget();

Containeris a great widget that you will use extensively in Flutter. Container()Extends to fit the constraints given by the parent class and is not consta constructor.

Therefore, when we have to implement placeholders, we should use SizedBoxinstead 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:

  • SizedBoxThere is a constconstructor, Containerwhich can result in more efficient code compared to
  • SizedBoxContaineris a lighter object than the one to instantiate .
  • SizedBox.shrink()Sets the width and height to 0, initialized to by default null.
    You can also use SizedBoxinstead of SizedBox.shrink, but using the word "shrink" makes it clear that this widget will take up minimal (or zero) space on the screen.
  • ContainerIf the widget has no children, no height, no width, no join constraints, and no alignment, but the parent provides bounded constraints, Containerit will expand to fit the constraints provided by the parent .

9. Use loginsteadprint

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 Debugused in modeprint

Make sure printand logstatements are only used in the application's Debugmode.

You can use kDebugModedetection Debugor Releasepattern

  • kReleaseMode, in Releasethe pattern istrue
  • kProfileMode, in Profilethe 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.';
  • ifSituations 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 setStatecalled, if Widgetwill not change, we should define it as a constant. It will prevent Widgetrebuilds of the , thus improving performance.

Also, Widgetusing consta 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. constDeclarations 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 constkeywords. Take a look at the code below:

const Container(
    width: 100,
    child: const Text('Hello World')
);

We don't need to Textuse for constbecause constit's already applied to the parent component.

Dart constprovides 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 varit 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/constclass 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.

  • awaitTry not to use keywords in blocks

  • Try not to use global variables and functions. They must be Classclosely 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.

List16. Using the Widget's Item Extentproperty in a long list

ItemExtentThis 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 itemExtentis 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.builderBuild a list with the same view using

  • Listview.builderCreated lists generate row views only as needed.
  • Listview.builderRepurpose an off-screen row view for a user-visible row view.
  • By default ListViewsrows 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 Widgetinto smaller Widgetcomponents helps reuse and improves performance.
  • Don't use functions for larger Widgetreturns Widget, it can lead to unnecessary calls to functions, which are expensive.
  • All derived ones will be regenerated when Statecalled 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()WidgetWidgetWidgetsetState()

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.allconstructors
  • avoid unnecessarysetState()
  • avoid returningwidgets
  • It is better to extract the callback callback
  • Preferably only one per fileWidget
  • It is better to use constant constdecorationborder-radius

22. Use FittedboxFlutter to implement responsive layout

To implement responsive design in Flutter, we can utilize FittedBoxcomponents.

FittedBoxis 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:

  1. FittedBoxWhen laying out subcomponents , the constraints passed by their parent components are ignored , and subcomponents can be allowed to be infinitely large , that is, FittedBoxthe constraints passed to subcomponents are ( 0 <= width <= double.infinity, 0 <= height <= double.infinity).

  2. FittedBoxThe real size of the subcomponent can be obtained after the layout of the subcomponent is completed .

  3. FittedBoxKnowing the real size of the child component and the constraints of its parent component , then FittedBoxyou can use the specified adaptation method ( BoxFitspecified in the enumeration) to make the child component FittedBoxdisplay 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 FittedBoxa wrapper container, it will accommodate the text according to the available size of the container. If the text exceeds FittedBoxthe 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:

  1. Code obfuscation
  2. 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) Rowand Columnthe layout setting the main axis alignment to spaceEvenlywill 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 Rowand Columnof to , can make the children close togethermainAxisSizeMainAxisSize.min , by default, the row or column will take up as much space as possible along its main axis.

  • 3) ExpandedOr Wrapthe component can solve the problem of interface overflow, Expandedyou can set flexthe ratio

  • 4) Each Elementcorresponds to one RenderObject, which we can Element.renderObjectobtain through . RenderObjectThe main responsibilities are layout and drawing, all of which RenderObjectwill form a rendering tree Render Tree .

  • 5) RenderObjectIt is an object in the rendering tree, which has one parentand one parentDataslot (slot). This slot is a reserved variable, mainly used to store childoffset data offset(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 is sizedByParent, but will be called every time the layout is made. trueperformResize()performLayout() sizedByParentIt means whether the size of the node can be determined only by parentpassed 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.constraintsparentsizedByParenttrueperformResize()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 ParentDatain the layoutoffset stage plus its own offset as the second parameter paintChild(), and if the child node has children When creating a node, paintChild()the method will also call the method of the child node paint(), 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 RenderObjectdrawing is very frequent or complex, it can be specified by Widget RepaintBoundary, so that only redraw itself and no need to redraw it when drawing , so that the performance can be improved.isRepaintBoundarytrueparent

  • 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 Centerand ListView.
    • Same size as child widgets , such as Transformand Opacityrender boxes.
    • Render boxes of specific sizes , such as Imageand .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) .

  • 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 widgetof the child cannot flexbe set to 0any value other than (the default). This means that in the widget library, it cannot be used when a flexbox is nested inside another flexbox or inside a scrollable area Expanded. 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 .flexRowflexwidget

  • 13) TextSet softwrapto true, 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 Stateis mutable.

  • 16) A StatefullWidgetkeeps track of its own internal state through an associated state object. StatefullWidgetis "dumb", widgetit is completely destroyed when it is removed from the tree.

  • 17) In Flutter, it is rendered widgetby 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) ContainerA component is a "convenience" widgetthat provides a large number of properties that you might otherwise need from each Widget.

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/131036924
Recommended