Flutter best practices

Amit Singh
8 min readAug 6, 2023

--

During your programming journey, you will build hell lots of applications irrespective of any technology that you use, and getting these things done is one thing but doing it the right way is what matters.

Flutter best practices

Hello guys, this is Amit Singh and in this post, we are going to discuss some of the best practices that you can apply while writing flutter/dart code.

[Background story]

So one day I was going through one of my old flutter application source codes. It took me some time to go through each module as the code was messy and unstructured.

But in between, I realized how much I have improved on flutter over time. So I decided to refactor my application and here are the lists of mistakes that I have made but you can avoid them.

New comers: Those of you who don’t know me. I write post on programming and share my flutter/Android Native journey from time to time. So if you are someone who is interested, make sure to follow and subscribe to my medium.

Alright so, If you are in rush you can consider these bullet points and leave, otherwise, feel free to read the explanation with an example.

  • Use the const keyword whenever possible
  • Use commas to intend your code
  • Follow naming convention
  • Use the right widget
  • Use private variable/method whenever possible
  • Proper use of state management
  • Avoid deep trees instead create a separate widget
  • Avoid method implementation within widget instead create a separate method
  • Avoid using nullable unless it is nullable
  • Use cascade (..)
  • Use spread operator (…)
  • define theme/route in a separate file
  • avoid using hardcoded strings (internationalized your app)
  • avoid using hardcoded style, decoration, etc.
  • Use relative import over package import within your app
  • Use a stateless widget whenever it is possible
  • Use flutter lint

Note: Keep in mind that there is nothing wrong with your application code. Following example explained the best and right way to implement the same logic. But coding with proper or well structured way you can archive manageable and long lasting code.

Using the const keyword whenever possible

Using a const keyword is always a good practice since it will avoid rebuilding the widget.

❌ Don’t

Column(
children: [
Text('One'),
Text('Two'),
],
)

✅ Do’s

Column(
children: const [
Text('One'),
Text('Two'),
],
)

Use commas to intend your code

You might want to add comma as many as possible to intend your application code properly.

❌ Don’t

@override
Widget build(BuildContext context) {
return const Scaffold(body: Center(child: Text('One')));
}

✅ Do’s

@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text(
'One'
),
),
);
}

Follow naming convention

In dart, there are things you need to keep in mind while naming such as

  1. file names always use camel case with a small case. eg. home_page.dart, file_manager.dart, etc
  2. class names start with cap. eg.
  3. method name start with small
  4. Use underscore if the field, method, or class is private

❌ Don’t

1. File name
Home_Page.dart
File_manager.dart2. Class name
class myApp
class myapp3. method name
void getdata() {}
void GetData() {}
4. Private variable
String privateVariable

✅ Do’s

1. File name
home_page.dart
file_manager.dart2. Class name
class MyApp3. method name
void getData() {}4. Private variable
String _privateVariable

Use the right widget

We all know flutter is all about widgets, but using the right widgets sometimes matters. for example, using a container widget just to get padding does not make any sense. use the Padding widget instead.

❌ Don’t

Container(
padding: const EdgeInsets.all(8),
child: const Text("Hello"),
),
Container(height: 10,),

✅ Do’s

const Padding(
padding: EdgeInsets.all(8),
child: Text("Hello"),
),
const SizedBox(height: 10,)

Flutter best practices

Use private variable/method whenever possible

Unless required, use a private keyword whenever possible.

❌ Don’t

class Student {
String name;
String address;
Student({
required this.name,
required this.address,
});
}

✅ Do’s

class Student{
String _name;
String _address;
Student({
required String name,
required String address,
}) : _name = name,
_address = address;
}

Proper use of state management

When you are not using any state management tool, you might need to rebuild your widget tree with setState but make sure you are using it the right way.

You don’t want want to rebuild the whole widget just because you want to update a single text field instead create a separate widget and rebuild that new widget only. It is also known as lifting the state up.

❌ Don’t

Column(
children: [
//long list of widget here
Text(
_textValue
),
ElevatedButton(
onPressed: (){
setState((){
_textValue="new value";
});
},
child: Text(
'Update'
),),
//long list of widget here
],
)

✅ Do’s

Column(
children: [
//long list of widget here
NewWidgetWithTextAndButton(),
//long list of widget here
],
)

Avoid deep trees instead create a separate widget

Trust me, I have faced this issue many times. You don’t want to keep on scrolling your IDE with a thousand lines of codes. Try creating a separate widget instead. It will look clean and easy to refactor.

❌ Don’t

Column(
children: [
Container(
//some lengthy code here
),
Container(
//some another lengthy code
),
//some another lengthy code
],
)

✅ Do’s

Column(
children: [
FirstLengthyCodeWidget(),
SecondLengthyCodeWidget(),
//another lengthy code widget etc
],
)

Avoid method implementation within widget instead create a separate method.

Do you have 50 lines of code within onPressed() on some button? Not a good practice. Wrap them in a method outside your widget.

❌ Don’t

@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
//long lengthy code here
},
child: const Text('Do some task'),
),
);
}

✅ Do’s

@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: _callTheListener,
child: const Text('Do some task'),
),
);
}
void _callTheListener(){
// your code here
}

Avoid using nullable unless you know it is nullable

One good thing about dart is that unlike java it supports null safety, In my old projects, I was getting lots of null exceptions since I wasn’t using null safety properly in my applications.

So avoid using nullable unless necessary.

❌ Don’t

TextEditingController? textEditingController;@override
initState() {
super.initState();
textEditingController = TextEditingController();
}

✅ Do’s

late TextEditingController textEditingController;
@override
initState() {
super.initState();
textEditingController = TextEditingController();
}

Use cascade (..)

If you are just starting with flutter, You might have not used this operator but it is very useful when you want to perform some task on the same object.

❌ Don’t

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

✅ Do’s

var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;

FLutter best practices

Use spread operator (…)

This is another beautiful operator that dart has to offer. You can simply use this operator to perform many tasks such as if-else, join the list, etc.

❌ Don’t

@override
Widget build(BuildContext context) {
bool isTrue = true;
return Scaffold(
body: Column(
children: [
isTrue ? const Text('One') : Container(),
isTrue ? const Text('Two') : Container(),
isTrue ? const Text('Three') : Container(),
],
),
);
}

✅ Do’s

@override
Widget build(BuildContext context) {
bool isTrue = true;
return Scaffold(
body: Column(
children: [
if(isTrue)...[
const Text('One'),
const Text('Two'),
const Text('Three')
]
],
),
);
}

Define route/theme in a separate file

You might want to define the route and theme in a separate file because you might end up requiring multiple themes in your application and many routes in your app.

❌ Don’t

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
textTheme: TextTheme(),
primaryColor: Colors.green,
//long list of other properties
),
home: const HomePage(),
);
}

✅ Do’s

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: appTheme,
home: const HomePage(),
);
}//In new file add your theme var appTheme = ThemeData(
primarySwatch: Colors.blue,
textTheme: const TextTheme(),
primaryColor: Colors.green,
//long list of other properties
);

Avoid using hardcoded strings (internationalized your app)

This might not look helpful but trust me when your application starts to grow, You will end up doing a lot of searches and replacing.

❌ Don’t

Text(
'one',
),

✅ Do’s

Text(
AppLocalizations.of(context)!.one,
),

Avoid using hardcoded style, decoration, etc.

If you are using a hardcoded style, decoration, etc in your application and later on if you decided to change those styles. You will be fixing them one by one.

❌ Don’t

Column(
children: const [
Text(
'One',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
Text(
'Two',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
)

✅ Do’s

Column(
children: [
Text(
'One',
style: Theme.of(context).textTheme.subtitle1,
),
Text(
'Two',
style: Theme.of(context).textTheme.subtitle1,
),
],
),

Use relative import over package import within your app

When you import a path in your file if you want to import an application class or file, It is recommended to use relative import over package import.

❌ Don’t

import 'package:flutter/material.dart';
import 'package:test_project/student.dart';

✅ Do’s

import 'package:flutter/material.dart';
import 'model/student.dart';

Use a stateless widget whenever it is possible

Avoid using the stateful widgets as much as you can, that way you don’t need to build the widget over again and again with setState.

❌ Don’t

class TestWidget extends StatefulWidget {
const TestWidget({Key? key}) : super(key: key);
  @override
State<TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
@override
Widget build(BuildContext context) {
//stateless content
return Image.asset('assets/men_walking.png');
}
}

✅ Do’s

class TestWidget extends StatelessWidget {
const TestWidget({Key? key}) : super(key: key);
  @override
Widget build(BuildContext context) {
//stateless content
return Image.asset('assets/men_walking.png');
}
}

Use records to return multiple value [dart 3 feature]

❌ Don’t

String getPersonName(Map<String, dynamic> json) {
return json['name'];
}
String getPersonAge(Map<String, dynamic> json) {
return json['age'];
}

✅ Do’s

(String, int) getPersonDetail(Map<String, dynamic> json) {
return (
json['name'],
json['age'],
);
}

Use enhance switch [dart 3 feature]

❌ Don’t

enum Type {
kid,
teen,
adult;
  int get discount {
switch (this) {
case kid:
return 30;
case teen:
return 20;
case adult:
return 5;
}
}
}
void main() {
print(Type.kid.discount);
}

✅ Do’s

enum Type {
kid,
teen,
adult;
   int get discount => switch (this) {
kid => 30,
teen => 20,
adult => 5,
};
}
void main() {
print(Type.kid.discount);
}

Use lint/flutter lint

These days, flutter provide flutter lint out of the box where you can define sets of rule for your code. Make sure to use it in your application.

✅ Do’s

flutter_lints: ^2.0.0

Flutter best practices

If you made till here I think you have done a great job learning all these best practices.

If any of my posts help you in any way, you can consider buying me a cup of coffee. I will personally thank you 🙏.

https://www.buymeacoffee.com/amit08cse53

Thanks, guys, That’s all for now, Make sure to give a clap 👏 and leave some engagement. If there is any correction or feedback feel free to leave a comment as well.

Check out similar posts from CodingWithAmitSingh. Connect me on Twitter or Linkedin

--

--