In my previous article on Flutter Tips and Tricks (Part 1/2) I talked about the very basic things that can help your Flutter app improve on performance. The six points I listed are:
Refactor your code into Widgets Instead of Methods
Use const keyword whenever possible
Using SizedBox instead of Container
Smart use of operators to reduce the number of lines for execution
Use relative imports instead of absolute imports
Use raw string
Now in this article I will list six more points which in my opinion are advanced compared to those of the article. If you will succed in implementing them in your project happy are you seeing their results they will bring you. Personally am yet to implement everything here as much as I am writing this for your and anyone out there.
1. Make the build function pure
Consider the following stateful widget:
class HelloWidget extends StatefulWidget {
const HelloWidget({
Key? key,
}) : super(key: key);
@override
_HelloWidgetState createState() => _HelloWidgetState();
}
class _HelloWidgetState extends State<HelloWidget> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: getText(),
builder: (context, AsyncSnapshot<String> snapshot) {
return Text(snapshot.data ?? '');
});
}
}
Here, when the rebuild happens the getText()
is called every time and this might create janky UI and use more resources.
So the idea of having pure build function is that we should move out any operation from build method that can affect rebuild performance.
2. Use a state management technique
Flutter itself doesn't impose any state management by default, so it's easy to end up with a messy combination and might depend on parameter passing or storing everything in persistent storage for storing state.
While using a simple solution for state management is always recommended, we should also consider the scalability and maintainability of the app to select it.
Furthermore, even though stateful widgets offer the simplest solution for state management, it can not scale when we want to maintain the state across multiple screens. e.g. Authentication state of User.
State management comes really handy here. It allows to have central store of things that we can use to store anything and when anything in store changes, all the widgets dependent on that will be changed automatically.
There are so many options available for state management. Depending on the experience and level of comfort of the team, we can use any of the available state management solutions:
Provider - A recommended approach.
Riverpod - another good choice, is similar to Provider and is compile-safe and testable. Riverpod doesn’t have a dependency on the Flutter SDK.
setState - The low-level approach to use for widget-specific, ephemeral state.
InheritedWidget & InheritedModel - The low-level approach used to communicate between ancestors and children in the widget tree. This is what provider and many other approaches use under the hood.
Redux - A state container approach familiar to many web developers
Fish-Redux - an assembled flutter application framework based on Redux state management. It is suitable for building medium and large applications.
BLoC / Rx - A family of stream/observable based patterns.
GetIt - A service locator based state management approach that doesn’t need a BuildContext.
MobX - A popular library based on observables and reactions.
Flutter Commands - Reactive state management that uses the Command Pattern and is based on ValueNotifiers. Best in combination with GetIt, but can be used with Provider or other locators too.
Binder - A state management package that uses InheritedWidget at its core. Inspired in part by recoil. This package promotes the separation of concerns.
GetX - A simplified reactive state management solution.
states_rebuilder - An approach that combines state management with a dependency injection solution and an integrated router. For more information, see the following info:
Triple Pattern (Segmented State Pattern) - A pattern for state management that uses Streams or ValueNotifier. This mechanism (nicknamed triple because the stream always uses three values: Error, Loading, and State), is based on the Segmented State pattern.
3. Have a well-defined Architecture
Since Flutter is a declarative framework, it's much easier to learn compared to native frameworks for Android and iOS. Also for Flutter, we just need to learn only one language for both design and code. But this can also lead to spaghetti code if we do not have well-defined architecture as things can get mixed up very easily.
At minimum we should have at least three layers:
While there can be many different options for the architecture, we should choose one that the team is most comfortable with. A bloc library offers a great set of examples with well-defined architecture.
4. Follow effective dart style guide
It's always better to have a defined style guide to have widely accepted conventions that can help improve the code quality. If we have a consistent style, it becomes much easier to have the team coordinated and even incorporate new developers. In this sense, maintaining a constant and regular style will help the big project in the long term.
While it's possible to define the custom style guide that the team is comfortable with, Dart also offers the official style guide that we can follow.
Moreover, it's always a great idea to have Linter in the project. This becomes very helpful when the team is large, when not everyone is aware of the style guide or when they forget to follow some rules. All the Linter rules offered by Dart can be found here.
5. Select packages carefully
The Flutter ecosystem is very supportive of the community, especially when it comes to reusable pieces of code. The typically called libraries are called packages in the Flutter ecosystem.
While it's always tempting to use a package for every functionality, we should consider the following factors before using any package:
When was the package updated? We should always avoid using stale packages.
How popular is the package? If the package has considerable popularity, it is much easier to find community support.
Check the open issues in the code repository of the package. Are there any issues the can affect the functionality we are trying to integrate from that package?
How frequently does the package get updated? This is really important if we want to take advantage of the latest Dart features.
Are you using only little functionalities from the package? It makes more sense to write the code or copy the code than depend on the whole package when we are using only a little functionality.
6. Write tests for critical functionality
While the possibility of relying on manual testing is always there, having an automated set of tests can save a considerable amount of time and effort. As Flutter targets multiple platforms, testing every single functionality after every change would be time-consuming and require a lot of repeated effort.
Ideally speaking, having 100% code coverage for testing is always the best option, however, it might not always be possible based on available time and budget. Nevertheless, it's necessary to have at least tests to cover the critical functionality of the app.
In addition, it's important to have integration tests that allow running tests on physical devices or emulators. Tip: we can also use Firebase Test lab for running tests on a variety of devices.
7. Some cosmetic points to keep in mind
Never fail to wrap your root widgets in a safe area.
You can declare multiple variables with shortcut- (int mark =10, total = 20, amount = 30;)
Ensure to use final/const class variables whenever there is a possibility.
Try not to use unrequired commented codes.
Create private variables and methods whenever possible.
Build different classes for colors, text styles, dimensions, constant strings, duration, and so on.
Develop API constants for API keys.
Try not to use of await keywords inside the bloc
Try not to use global variables and functions. They have to be tied up with the class.
Check dart analysis and follow its recommendations
Check underline which suggests Typo or optimization tips
Use _ (underscore) if the value is not used inside the block of code.
Don't ...
someFuture.then((DATA_TYPE VARIABLE) => someFunc());
Instead do ...
someFuture.then((_) => someFunc());
- Magical numbers always have proper naming for human readability.
Don't ...
SvgPicture.asset(
Images.frameWhite,
height: 13.0,
width: 13.0,
);
Instead do ...
final _frameIconSize = 13.0;
SvgPicture.asset(
Images.frameWhite,
height: _frameIconSize,
width: _frameIconSize,
);
So, this was about the best practices for Flutter development that relatively ease down the work of every Flutter developer while also improving their app’s performance. Happy coding!
Don't fail to Follow me here and On
Twitter @JacksiroKe
Linked In Jack Siro
Github @JacksiroKe