- Flutter Style Guide
- DON'T use functions which return widgets
- DO use const widgets where possible
- DON'T use runtimeType for type checking
- DON'T subscribe to all MediaQuery changes
- Prefer Object over dynamic
- DO use SizedBox instead of Container
- DO use prototypeItem in ListView for long lists
- DO use if condition instead of ternary operator syntax when you need render widget conditionally
- StatefulWidget: When to call super.initState() and super.dispose()?
- DO always close streams when you are done with them
- DO always dispose of AnimationControllers when you are done with them
- DO always dispose of ScrollControllers when you are done with them
- Common constants approach
- DO Avoiding large trees of widgets
- DO always treat warnings as errors
Composing a tree using functions that return widgets has the same performance characteristics as a single large build method that returns all those widgets inline. Composing a tree using widgets is significantly better since it localizes rebuilds.
To make up for some misunderstanding: This is not about functions causing problems, but classes solving some.
Flutter wouldn't have StatelessWidget if a function could do the same thing.
Similarly, it is mainly directed at public widgets, made to be reused. It doesn't matter as much for private functions made to be used only once – although being aware of this behavior is still good.
There is an important difference between using functions instead of classes, that is: The framework is unaware of functions, but can see classes.
Consider the following "widget" function:
Widget functionWidget({ Widget child}) {
return Container(child: child);
}
used this way:
functionWidget(
child: functionWidget(),
);
And it's class equivalent:
class ClassWidget extends StatelessWidget {
final Widget child;
const ClassWidget({Key? key, this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: child,
);
}
}
used like that:
const ClassWidget(
child: ClassWidget(),
)
In theory, both seem to do exactly the same thing: Create 2 Container, with one nested into the other. But the reality is slightly different.
In the case of functions, the generated widget tree looks like this:
Container
Container
While with classes, the widget tree is:
ClassWidget
Container
ClassWidget
Container
This is important because it changes how the framework behaves when updating a widget.
Why that matters By using functions to split your widget tree into multiple widgets, you expose yourself to bugs and miss on some performance optimizations.
There is no guarantee that you will have bugs by using functions, but by using classes, you are guaranteed to not face these issues.
Here are a few interactive examples on DartPad that you can run yourself to better understand the issues:
-
This example showcases how by splitting your app into functions, you may accidentally break things like AnimatedSwitcher
-
This example showcases how classes allow more granular rebuilds of the widget tree, improving performances: https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1
-
This example showcases how, by using functions, you expose yourself to misusing BuildContext and facing bugs when using InheritedWidgets (such as Theme or providers): https://dartpad.dev/06842ae9e4b82fad917acb88da108eee
Conclusion
Classes:
- allow performance optimization (const constructor, more granular rebuild)
- ensure that switching between two different layouts correctly disposes of the resources (functions may reuse some previous state)
- ensures that hot-reload works properly (using functions could break hot-reload for showDialogs & similar)
- are integrated into the widget inspector.
- we can see ClassWidget in the widget-tree showed by the devtool, which helps understanding what is on screen
- We can override debugFillProperties to print what the parameters passed to a widget are
- better error messages
- If an exception happens (like ProviderNotFound), the framework will give you the name of the currently building widget. If you've split your widget tree only in functions + Builder, your errors won't have a helpful name
- Can define keys
- Can use the context API
Functions:
- have a better shape and less code.
Utilizing const constructors when creating widgets in Flutter can offer significant performance optimizations.
Below are the key points concerning the use of const widgets:
Performance Optimization:
const
constructors allow Flutter to reuse widgets across builds, which significantly optimizes performance by reducing the amount of widget rebuilding necessary.
The framework can quickly compare const widgets and determine whether the widget tree needs to be updated, saving both memory and CPU cycles.
Code Maintainability:
const
widgets make the immutability of the widget explicit, which is a good practice for maintaining a clear, understandable codebase.
It encourages the use of immutable data structures, aligning with Flutter’s paradigm of immutable widget trees and functional reactive programming.
Compile-time Safety:
Using const allows for some errors to be caught at compile-time rather than runtime, which is safer and can prevent bugs from reaching production.
Example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const AppBar( // const constructor
title: Text('Const Example'),
),
body: const Center( // const constructor
child: Text('Hello, World!'),
),
);
}
}
In this example:
const
constructors are used to create AppBar
and Center
widgets.
By specifying const
, we're telling Flutter that these widgets will never change and can be reused across builds,
optimizing the performance of our app.
RuntimeType
is only for debugging purposes and the application code shouldn't depend on it.
It can be overridden by classes to return fake values and probably returns unusable values.
Instead use instance of operator is
:
class Foo { }
main() {
final foo = Foo();
if (foo is Foo) {
print('it's a foo!');
}
}
MediaQuery
provides access to many aspects of the platform the Flutter app is running on. Querying using MediaQuery.of will cause your widget to rebuild automatically whenever any field of the MediaQueryData changes (e.g., if the user rotates their device). However most use cases require rebuilding your widget only when a single property changes, such as the window size when a user resizes the browser. Therefore, unless you are concerned with the entire MediaQueryData object changing, prefer using the specific methods (for example: MediaQuery.sizeOf and MediaQuery.paddingOf), as it will rebuild more efficiently.
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
// DON'T:
width: MediaQuery.of(context).size.width * 0.5,
// DO:
width: MediaQuery.sizeOf(context).width * 0.5,
)
}
}
The dynamic type is special. It really means Trust me, I know what I'm doing
and it turns off some static type checking.
Another perspective on dynamic
is that it's not really a type - it's a way to turn off type checking and tell the static type system "trust me, I know what I'm doing".
Writing dynamic o
; declares a variable that isn't typed - it's instead marked as "not type-checked".
When you write Object o = something
; you are telling the system that it can't assume anything about o except that it's an Object
.
You can call toString
and hashCode
because those methods are defined on Object
,
but if you try to do o.foo()
you will get a warning - it can't see that you can do that, and so it warns you that your code is probably wrong.
If you write dynamic o = something
you are telling the system to not assume anything, and to not check anything.
If you write o.foo()
then it will not warn you.
You've told it that "anything related to o is OK! Trust me, I know what I'm doing", and so it thinks o.foo()
is OK.
With great power comes great responsibility - if you disable type-checking for a variable, it falls back on you to make sure you don't do anything wrong.
Additional Notes:
- In Dart, there is no way to distinguish the type
dynamic
and the typeObject
atrun-time
, because there is no difference. - The difference between
dynamic
andObject
exists only at compile-time, where you are allowed to call any method on a value with static type dynamic, and not very many methods on something with static typeObject
. - Using
dynamic
typed variables in Dart is often slower than using variables typed with an actual type. A dynamic method invocation may be slower because the run-time system must add extra checks to ensure that the variable can do the things you are trying to do with it.
In Flutter, both the SizedBox
and Container
widgets can be utilized to set the dimensions of a child widget.
However, when the sole intention is to specify a fixed width and/or height, it's often more straightforward and expressive to use SizedBox
instead of Container
.
Here are the key points concerning this guidance:
Expressive Clarity:
The SizedBox widget is explicit in its purpose— to provide fixed dimensions. Using SizedBox makes it clear to other developers that the goal is to specify dimensions.
Simplicity:
SizedBox is a simpler widget with fewer properties compared to Container, making it a more direct choice when the only requirement is to set dimensions.
Performance: Although the performance difference might be negligible, using a simpler widget could be slightly more performant since it has fewer properties and thus a smaller footprint.
Code Conciseness: SizedBox offers a more concise way to specify dimensions when you don’t need the additional capabilities of Container.
Example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
// Using SizedBox to specify dimensions
child: SizedBox(
width: 100.0,
height: 100.0,
child: FlutterLogo(),
),
),
),
);
}
}
By employing SizedBox
when the task is merely to assign fixed dimensions,
you adhere to a practice that promotes code simplicity and readability.
When dealing with long lists, utilizing the prototypeItem property in ListView can be beneficial. This property allows Flutter to measure the dimensions of list items without having to inflate them all, which can lead to performance optimizations.
Here's a breakdown of this guidance:
Performance Optimization: By providing a prototypeItem, Flutter can efficiently measure item dimensions, which is particularly useful for long lists where inflating all items can be performance-intensive.
Consistency: When all items in the list have uniform dimensions, specifying a prototypeItem ensures that Flutter has a consistent size to work with, optimizing layout calculations.
Resource Efficiency: It can save resources as the framework doesn't have to create and dispose of widgets just to measure them.
Example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item #$index'),
);
},
prototypeItem: const ListTile(
title: Text(''),
),
),
),
);
}
}
In this example:
- A
ListView.builder
is used to create a long list of 1000 items. - The prototypeItem property is set to a
ListTile
widget, which serves as a prototype for measuring the dimensions of list items. - This usage allows Flutter to efficiently calculate item dimensions without having to inflate every item, which can lead to performance improvements, especially in cases of long lists.
Utilizing the prototypeItem
property in ListView
when dealing with long lists is a good practice for optimizing performance and
ensuring resource efficiency in your Flutter applications.
In the process of laying out a Flutter app, there are often scenarios where rendering different widgets conditionally is required. For instance, you might need to generate a widget based on the platform:
Row(
children: [
Text('Platform'),
Platform.isAndroid ? Text('Android') : SizeBox(),
Platform.isIOS ? Text('iOS') : SizeBox(),
]
);
In this scenario, you can forego the ternary operator and take advantage of Dart's inherent syntax to incorporate an if
statement within an array:
Row(
children: [
Text('Platform'),
if (Platform.isAndroid) Text('Android'),
if (Platform.isIOS) Text('iOS'),
]
);
You can also extend this functionality using a spread operator to load multiple widgets as required:
Row(
children: [
Text('Platform'),
if (Platform.isAndroid) Text('Android'),
if (Platform.isIOS) ...[
Text('Text 1'),
Text('Text 2'),
],
]
);
super.initState()
should always be the first line in your initState method.
Flutter docs: If you override this, make sure your method starts with a call to super.initState().
@override
void initState() {
super.initState();
// DO STUFF
}
super.dispose()
should always be the last line in your dispose method.
Flutter docs: If you override this, make sure to end your method with a call to super.dispose().
@override
void dispose() {
// DO STUFF
super.dispose();
}
Resource Management and Memory Leaks:
Streams, especially those associated with IO-bound work like reading from a file or a network socket, use up system resources. When a stream is open, it holds onto memory and potentially other resources like file handles or network connections. If not closed, these resources remain allocated, leading to memory leaks and potentially exhausting system resources which could affect the performance of your application or even cause it to crash.
Error Prevention and Data Integrity:
Not closing a stream can lead to bugs or data corruption. For instance, if you're writing to a file through a stream and forget to close the stream, some data might remain buffered and not get written to the file. This can lead to data loss or corrupted files.
Good Practice:
Explicitly closing streams is considered good practice in Dart and Flutter, as it shows that you're managing resources correctly which is crucial for building reliable and efficient applications.
Suppose you have a stream that emits values from a user input field and you process these values in some way.
import 'dart:async';
class InputHandler {
final StreamController<String> _inputStreamController = StreamController<String>();
void onUserInput(String input) {
_inputStreamController.sink.add(input); // Sending data into the stream
}
void processInput() {
_inputStreamController.stream.listen((input) {
// Process the user input in some way
print('Processed input: $input');
});
}
void dispose() {
_inputStreamController.close(); // Closing the stream when done
}
}
void main() {
final inputHandler = InputHandler();
inputHandler.processInput();
// Simulate user input
inputHandler.onUserInput('Hello, World!');
// Dispose of resources when done
inputHandler.dispose();
}
In this example:
- We have defined a class
InputHandler
with aStreamController
_inputStreamController
to handle the user input. - The
onUserInput
method simulates receiving user input and adds this input to the stream. - The processInput method sets up a listener on the stream to process the user input.
- The
dispose
method is crucial; it's where we close the_inputStreamController
, thus releasing any resources held by the stream. - In the main function, we create an instance of
InputHandler
, simulate some user input, process the input, and finally calldispose
to clean up the resources when we're done.
In Flutter, AnimationController
s are used to drive animations.
However, they consume system resources and hence it is crucial to dispose of them once they are no longer needed.
Below are some key points and an example illustrating this practice:
Resource Management:
Disposing of AnimationController
instances when they are no longer needed helps in
freeing up system resources which can lead to better performance.
Preventing Memory Leaks:
If AnimationController
s are not disposed of,
they can cause memory leaks which may degrade the performance of the application over time.
Adhering to Best Practices:
Properly managing resources by disposing of objects like AnimationController
is a good programming practice in Flutter,
making your code more robust and maintainable.
Example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
}
@override
void dispose() {
_animationController.dispose(); // Disposing of the AnimationController
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Example'),
),
body: Center(
child: FadeTransition(
opacity: _animationController,
child: const FlutterLogo(size: 100.0),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_animationController.forward();
},
child: Icon(Icons.play_arrow),
),
);
}
}
By properly disposing of the AnimationController, you ensure that the system resources are freed up when they are no longer needed, adhering to good resource management practices in Flutter.
In Flutter, a ScrollController
is used to control the position of a scrollable widget.
Disposing of a ScrollController
when it's no longer needed is crucial for several reasons:
Resource Management:
ScrollController
holds onto resources that need to be freed up to ensure the efficiency of your application.
Memory Leaks Prevention:
If not disposed of, a ScrollController
can cause memory leaks which would degrade the performance of your application over time.
Error Prevention:
It's possible to encounter errors or unexpected behaviors if you try to interact with a ScrollController
that should have been disposed.
Adherence to Best Practices:
Properly managing resources by disposing of objects like ScrollController
is a good programming practice in Flutter,
making your code more robust and easier to maintain.
Here's an example illustrating how to properly dispose of a ScrollController
:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose(); // Dispose of the ScrollController
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ScrollController Example'),
),
body: ListView.builder(
controller: _scrollController,
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item #$index'),
);
},
),
);
}
}
In this example:
- A new
ScrollController
instance is created in the initState method. - The dispose method is overridden to ensure that
_scrollController
is disposed of whenMyHomePage
is removed from the widget tree. - The
_scrollController
is passed to theListView.builder's
controller property to control the scroll position of the list.
This way, the ScrollController
is properly disposed of, preventing any potential resource leaks or errors.
- Use
_k
prefixes to all of public and private constants - Constant classes should be named
_C
or containConst
in the end
const _kHorizontalPadding = 4.0;
abstract class NewConst {
static const _kHost = 'www.example.com';
}
abstract class _C {
static const topPaddingWeb = 100.0;
}
abstract class _NewItemConst {
static const drep = 'dRep';
static const publicKey = 'PUBLIC_KEY';
static const wallet = 'WALLET';
}
Treating warnings as errors in a software development project can be a highly beneficial practice.
Early Detection of Potential Issues:
- Warnings are often indicative of potential problems in the code. By treating warnings as errors, developers are forced to address these issues early on, which can prevent bugs from manifesting later.
Code Quality:
This practice encourages cleaner, more robust code by ensuring that developers address not only blatant errors but also other suboptimal coding practices that might generate warnings.
Maintainability:
Codebases with fewer warnings are generally easier to maintain and understand. Addressing warnings promptly keeps the codebase tidy and reduces technical debt.
Consistency:
Enforcing a policy where warnings are treated as errors can lead to more consistent coding practices across a development team.
Education and Awareness:
Sometimes, warnings alert developers to deprecated APIs or newer, better practices. Treating warnings as errors can be educational for developers and promote awareness of evolving best practices. Example:
In Dart/Flutter, you can treat warnings as errors by adding the following line to your analysis_options.yaml
file:
analyzer:
errors:
unused_local_variable: error
deprecated_member_use: error
In this example, the unused_local_variable
and deprecated_member_use
warnings are promoted to errors.
Now, whenever the Dart analyzer detects an unused local variable or the use of a deprecated member,
it will report these as errors, forcing the developer to address these issues before proceeding.
This is a way to ensure that the code adheres to certain quality standards and potential issues are addressed promptly.