Flutter is one of the most popular cross-platform mobile frameworks by Google. The developers have widely adopted the framework across the world, hence there is a loop of updated versions of Flutter, and the latest is Flutter 3. 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.
Here, you will learn the best practices for Flutter developers to improve code quality, readability, maintainability, and productivity. Let’s get cracking:
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:
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
},
);
}
}
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.
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();
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);
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;
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';
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) {
// ...
}
}
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.
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 $';
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';
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();
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');
String alert = isReturningCustomer ? 'Welcome back to our site!' : 'Welcome, please sign up.';
Widget getText(BuildContext context) {
return Row(
children:
[
Text("Hello"),
if (Platform.isAndroid) Text("Android") (here if you use ternary then that is wrong)
]
);
}
//Do
const SizedBox(height: Dimens.space_normal)
//Do not
SizedBox(height: Dimens.space_normal)
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;
//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;
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
//Do
someFuture.then((_) => someFunc());
//Do not
someFuture.then((DATA_TYPE VARIABLE) => someFunc());
//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,
);
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, consult with us. We have a proficient team of Flutter developers to assist you with your project.
Launching a successful mobile app startup is a real deal as many struggle to find…
Digital transformation in the banking industry is one of the most lucrative ways for financial…
Angular vs React: Which is best for web development has been a hot topic of…
This website uses cookies.