12 Best Practices to Simplify Flutter App Development in 2025
- Mobile
- October 29, 2023
Flutter is one of the most popular cross-platform mobile frameworks by Google. Developers, beyond those providing Flutter development services, are increasingly adopting the Flutter framework for a wide range of mobile app projects worldwide. As a result, Flutter frequently releases updated versions, with Flutter 3 being the latest. Today we are going to talk about what are the best practices for Flutter app development, referring to this blog will simplify your process of developing an app with Flutter.
Flutter Best Practice for App Development
Here, you will learn the best practices for Flutter developers to improve code quality, readability, maintainability, and productivity. Let’s get cracking:
1. Make the build function pure
The build method is developed in such a way that it has to be pure/without any unwanted stuff. This is because there are certain external factors that can trigger a new widget build, below are some examples:
- Route pop/push
- Screen resize, usually because of keyboard appearance or orientation change
- The parent widget recreated its child
- An Inherited Widget the widget depends on (Class. of(context) pattern) change
Avoid:
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: httpCall(),
builder: (context, snapshot) {
// create some layout here
},
);
}
Should be like this:
class Example extends StatefulWidget {
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<int> future;
@override
void initState() {
future = repository.httpCall();
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
}
2. Understanding the concept of constraints in Flutter
There is a thumb rule of a Flutter layout that every Flutter app developer needs to know: constraints go down, sizes go up, and the parent sets the position. Let’s understand more about the same:
A widget has its own constraints from its parent. A constraint is known to be a set of four doubles: a minimum and maximum width, and a minimum and maximum height.
Next up, the widget goes through its own list of children. One after another, the widget commands its children what their constraints are (which can be different for each child), and then asks each child what size it wants to be.
Next, the widget positions its children (horizontally in the x axis, and vertically in the y axis) one after the other. And then, the widget notifies its parent about its own size (within the original constraints, of course).
In Flutter, all widgets give themselves on the basis of their parent or their box constraints. But this has some limitations attached.
For instance, if you have got a child widget inside a parent widget and you would want to decide on its size. The widget cannot have any size on its own. The size of the widget must be within the constraints set by its parent.
3. Smart use of operators to reduce the number of lines for execution
- Use Cascades Operator
If we are supposed to perform a sequence of operations on the same object then we should opt for Cascades(..) operator.
//Do
var path = Path()
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, 0)
..close();
//Do not
var path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
path.close();
- Use spread collections
You can use spread collections when existing items are already stored in another collection, spread collection syntax leads to simpler and easier code.
//Do
var y = [4,5,6];
var x = [1,2,...y];
//Do not
var y = [4,5,6];
var x = [1,2];
x.addAll(y);
- Use Null safe (??) and Null aware (?.) operators
Always go for ?? (if null) and ?. (null aware) operators instead of null checks in conditional expressions.
//Do
v = a ?? b;
//Do not
v = a == null ? b : a;
//Do
v = a?.b;
//Do not
v = a == null ? null : a.b;
- Avoid using “as” operator instead of that, use “is” operator
Generally, the as
cast operator throws an exception if the cast is not possible. To prevent an exception being thrown, one can use `is
`.
//Do
if (item is Animal)
item.name = 'Lion';
//Do not
(item as Animal).name = 'Lion';
4. Use streams only when needed
While Streams are pretty powerful, if we are using them, it lands a big responsibility on our shoulders in order to make effective use of this resource.
Using Streams with inferior implementation can lead to more memory and CPU usage. Not just that, if you forget to close the streams, you will lead to memory leaks.
So, in such cases, rather than using Streams, you can use something more that consumes lesser memory such as ChangeNotifier for reactive UI. For more advanced functionalities, we can use Bloc library which puts more effort on using the resources in an efficient manner and offer a simple interface to build the reactive UI.
Streams will effectively be cleaned as long as they aren’t used anymore. Here the thing is, if you simply remove the variable, that is not sufficient to make sure it’s not used. It could still run in the background.
You need to call Sink.close() so that it stops the associated StreamController, to make sure resources can later be freed by the GC.
To do that, you have to use StatefulWidget.dispose of method:
abstract class MyBloc {
Sink foo;
Sink bar;
}
class MyWiget extends StatefulWidget {
@override
_MyWigetState createState() => _MyWigetState();
}
class _MyWigetState extends State<MyWiget> {
MyBloc bloc;
@override
void dispose() {
bloc.bar.close();
bloc.foo.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
// ...
}
}
5. Write tests for critical functionality
The contingencies of relying on manual testing will always be there, having an automated set of tests can help you save a notable amount of time and effort. As Flutter mainly targets multiple platforms, testing each and every functionality after every change would be time-consuming and call for a lot of repeated effort.
Let’s face the facts, having 100% code coverage for testing will always be the best option, however, it might not always be possible on the basis of available time and budget. Nonetheless, it’s still essential to have at least tests to cover the critical functionality of the app.
Unit and widget tests are the topmost options to go with from the very beginning and it’s not at all tedious as compared to integration tests.
6. Use raw string
A raw string can be used to not come across escaping only backslashes and dollars.
//Do
var s = r'This is demo string and $';
//Do not
var s = 'This is demo string \ and $';
7. Use relative imports instead of absolute imports
When using relative and absolute imports together then It is possible to create confusion when the same class gets imported from two different ways. To avoid this case we should use a relative path in the lib/ folder.
//Do
import '../../themes/style.dart';
//Do not
import 'package:myapp/themes/style.dart';
8. Using SizedBox instead of Container in Flutter
There are multiple use cases where you will require to use a placeholder. Here is the ideal example below:
return _isNotLoaded ? Container() : YourAppropriateWidget();
The Container is a great widget that you will be using extensively in Flutter. Container() brodens up to fit the constraints given by the parent and is not a const constructor.
On the contrary, the SizedBox is a const constructor and builds a fixed-size box. The width and height parameters can be null to specify that the size of the box should not be constrained in the corresponding dimension.
Thus, when we have to implement the placeholder, SizedBox should be used rather than using a container.
return _isNotLoaded ? SizedBox() : YourAppropriateWidget();
9. Use log instead print
print() and debugPrint() both are always applied for logging in to the console. If you are using print() and you get output which is too much at once, then Android discards some log lines at times.
To not face this again, use debugPrint(). If your log data has more than enough data then use dart: developer log(). This enables you to add a bit more granularity and information in the logging output.
//Do
log('data: $data');
//Do not
print('data: $data');
- Use ternary operator for single-line cases.
String alert = isReturningCustomer ? 'Welcome back to our site!' : 'Welcome, please sign up.';
- Use if condition instead of ternary operator for the case like below.
Widget getText(BuildContext context) {
return Row(
children:
[
Text("Hello"),
if (Platform.isAndroid) Text("Android") (here if you use ternary then that is wrong)
]
);
}
- Always try to use const widgets. The widget will not change when setState call we should define it as constant. It will impede the widget from being rebuilt so it revamps performance.
//Do
const SizedBox(height: Dimens.space_normal)
//Do not
SizedBox(height: Dimens.space_normal)
10. Don’t explicitly initialize variables null
In Dart, the variable is intuitively initialized to null when its value is not specified, so adding null is redundant and unrequired.
//Do
int _item;
//Do not
int _item = null;
- Always highlight the type of member when its value type is known. Do not use var when it is not required. As var is a dynamic type takes more space and time to resolve.
//Do
int item = 10;
final Car bar = Car();
String name = 'john';
const int timeOut = 20;
//Do not
var item = 10;
final car = Car();
const timeOut = 2000;
11. Use the const keyword whenever possible
Using a const constructor for widgets can lessen down the work required for garbage collectors. This will probably seem like a small performance in the beginning but it actually adds up and makes a difference when the app is big enough or there is a view that gets often rebuilt.
Const declarations are also more hot-reload friendly. Moreover, we should ignore the unnecessary const keyword. Have a look at the following code:
const Container(
width: 100,
child: const Text('Hello World')
);
We don’t require to use const for the Text widget since const is already applied to the parent widget.
Dart offers following Linter rules for const:
prefer_const_constructors
prefer_const_declarations
prefer_const_literals_to_create_immutables
Unnecessary_const
12. 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.
//Do
someFuture.then((_) => someFunc());
//Do not
someFuture.then((DATA_TYPE VARIABLE) => someFunc());
- Magical numbers always have proper naming for human readability.
//Do
final _frameIconSize = 13.0;
SvgPicture.asset(
Images.frameWhite,
height: _frameIconSize,
width: _frameIconSize,
);
//Do not
SvgPicture.asset(
Images.frameWhite,
height: 13.0,
width: 13.0,
);
Alright, here we are
So, this was about the best practices for Flutter development that relatively ease down the work of every Flutter developer.
If you’re having difficulty developing a Flutter app or want to hire dedicated Flutter developers for your project, MindInventory can be your ideal destination for the same. We have a proficient team of Flutter developers to assist you with your project.