Learning Flutter
Flutter nowadays become a popular choice among the community due to its capability of creating apps in record time. In this article, it explains some of the critical best practises to follow which helps developer work with the framework effectively and get things right from the beginning. The tips below are summarized from various blogs, articles as well as the official website for dart and flutter. Among all the referenced websites there are hundreds of topics which provide flutter development suggestions, the tips in this article are choosen based on the following criterias:
- Each of the tips is focusing on solving a specific issue within limited scope. Discussions related to design patterns, app architectures which involves in large volumes of background information and the anwser varies from different use cases are not included.
- Each of the tips talks about methodology to solve practical problems which developers will very likely meet during development process. However, common suggestions which are either already well-known to the community or developers can get easily from dart and flutter official websites are not included.
Best Practises
Tips: Naming Convention Summary
First of all, as a profecient dart language user is to obey the naming convention, which makes it easier for us to learn from and contibute to each other’s code, increase the long term maintainability of the project you works on.
Identifiers | Naming Convention |
---|---|
types | ‘UpperCamelCase’ |
libraries/packages/dir/source files | ‘lower_case_underscore’ |
acronyms & abbreviation | Capitalize when longer than two letters like words ‘HttpConnectionInfo’, ‘uiHandler’ |
prefixes | Avoid using prefix letters, since dart already supports type, scope, mutability declaration |
Tips: Directives Ordering
To keep the preamnble of the code tidy, import and export declaration is suggested to take the following order:
// place dart: imports before others
import 'dart:async';
import 'dart:html';
// followed by: third party libraries
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
// followed by: internal modules/packages
import 'package:my_package/util.dart';
// followed by: exports in seperate section after all imports
export 'src/error.dart';
Tips: Use ‘null-aware operators’
These grammar sugar servers as a powerful shortcuts for longer expressions.
// assign a to b if b is null;
b ??= a;
// return b if b is no-null; otherwise return a;
b ?? a;
With similar characters in other languages such as swift and kotlin,optional chaining is supported to null safe param accessing and method invocation. Since dart 2.3, spread operator also fully supports null-aware operator form.
// optional chaining
obj?.child?.child?.method();
// null-aware spread operator
List result = […$nullableArr1?…$nullableArr2];
Tips: Use ‘==’ for equality and ‘identical()’ for identity.
Developers can override the ‘==’ operator to define what equality means. Where using ‘identical()’ to check if two variables reference the same interface.
class Person {
String ssn;
String name;
Person(this.ssn, this.name);
// Define that two persons are equal if their SSNs are equal
bool operator ==(other) {
return (other is Person && other.ssn == ssn);
}
}
main() {
var bob = new Person('111', 'Bob');
var robert = new Person('111', 'Robert');
print(bob == robert); // true
print(identical(bob, robert)); // false
}
Tips: Implicit Interfaces
Every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements. This reduces the trouble to redeclare abstract interfaces when some of the existing module already provide a consize and clean method declaration:
// A person. The implicit interface contains greet().
class Person {
// In the interface, but visible only in this library.
final _name;
// Not in the interface, since this is a constructor.
Person(this._name);
// In the interface.
String greet(String who) => 'Hello, $who. I am $_name.';
}
// An implementation of the Person interface.
class Impostor implements Person {
get _name => '';
String greet(String who) => 'Hi $who. Do you know who I am?';
}
Tips: Lazy Loading Library
Like many other languages, the ‘static’ keyword can be used to lazy loading statical class-wide variables. In the meanwhile there is a technique called ‘deferred loading’, which allows an application to load a library on demand and provides the following benefits:
- Reduce the app’s initial startup time.
- A/B testing between alternative libraries.
// Define lazy loading with deferred keyword
import 'package:greetings/hello.dart' deferred as hello;
// Invoke loadLibrary() to load the lib during run time
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
Tips: Use ‘Zone’ for Error Isolation
Currently, the most common use of zones is to handle errors raised in asynchronously executed code. For example, a simple HTTP server might use the following code:
runZoned(() {
HttpServer.bind('0.0.0.0', port).then((server) {
server.listen(staticFiles.serveRequest);
});
},
onError: (e, stackTrace) => print('Oh noes! $e $stackTrace'));
Zone makes the following tasks possible:
- Protecting app from exiting due to uncaught exception thrown by async code.
- Overriding limited set of system methods such as ‘print’ & ‘scheduleMicroTask’ within the scope of a zone.
Tips: About Constant Constructor
If you have a class where all the fields are final, and the constructor does nothing but initialize them, you can make that constructor const. That lets users create instances of your class in places where constants are required—inside other larger constants, switch cases, default parameter values, etc.
class User {
final String name;
const User(this.name);
}
main() {
const users = const {
'Matan': const User(
'Matan Lurey',
),
};
}
Tips: Use ‘isEmpty/isNotEmpty’ over ‘.length’
Calling .length just to see if the collection contains anything in Dart can be painfully slow. Instead, using ‘isEmpty/isNotEmpty’ for it:
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
Tips: Use adjacent strings and interpolation for string composing.
If you have two string literals—not values, but the actual quoted literal form—you do not need to use + to concatenate them. Similar to other languages, iterpolation is also supported in dart.
// adjacent string concantenation
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.');
// interpolation
'Hello, $name! You are ${year - birth} years old.';
Tips: Prefer ‘toList’ instead of ‘List.from’
Given an Iterable, there are two obvious ways to produce a new List that contains the same elements, where the important difference is that the first one preserves the type argument of the original object :
var iterable = [1, 2, 3];
// runTimeType: List<int>
var copy1 = iterable.toList();
// runTimeType: List<dynamic>
var copy2 = List.from(iterable);
Tips: Use ‘WhereType’ to filter collection elements
Instead of using combination of ‘where()’ and ‘cast()’ to filter out collection of a single type from a collection containing mixture of objects, dart’s provide a more concise way of using ‘whereType’:
var objects = [1, "a", 2, "b", 3];
// discouraged way
var ints1 = objects.where((e) => e is int).cast<int>();
// encouraged way
var ints2 = objects.whereType<int>();
Tips: Use ‘ListView.seperated’ to fast build list seperators
In flutter, creating a Listview
can be straightforward. Using the code below, developers can build a simple listview.
ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.all(4.0),
child: Center(child: Text("Item $index")),
),
)
Since flutter provides almost everything a normal application would need, it provides helper constructor for creating list view with dividers in few lines of code:
ListView.separated(
separatorBuilder: (context, index) => Divider(
color: Colors.black,
),
itemCount: 10,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.all(4.0),
child: Center(child: Text("Item $index")),
),
)
Which can build the app into something like:
Tips: About Logs
You can use the print() function to view it in the system console. If your output is too much, then Android sometimes discards some log lines. To avoid this, in any flutter widget, you can use debugPrint().
Further more, developer can also print calls to disk for long-term execution or background tasks:
//
void runLoggedTasks(Function task, [String filename = 'log.txt']) async {
final log = File(p.join((await getApplicationDocumentsDirectory()).path, filename)).openWrite(mode: FileMode.append);
runZoned(() {
task();
}, zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
log.writeln('${DateTime.now().toIso8601String()} | $line');
log.flush();
parent.print(line);
},
));
}
Tips: Conversion between units of UI measurement.
Currently flutter is adopting its own unit of measurement for screen dimensions ‘logical pixel’ and a property to represent number of device pixels for each logical pixel. To integrate the flutter UI into existing iOS and Android apps, developers need to keep in mind that the units of measuremets between flutter, iOS and Android is totally different, where Android UI design relies heavily on dependent pixels and iOS’s UI design is based on UIKit Size, aka point. Here is the cheatsheet below:
Technique | Measurement Unit | Definition |
---|---|---|
Native Android | ‘dp’ (density independent pixel) | 160 dp per inch |
Native iOS | ‘point’ (UIKit size) | the points per inch varies based on scale factor. following formula: points * scaling_factor = physical pixel size. the PPI varies from device to device |
Flutter | ‘logical pixel’ | 38 logical pixels per centimeter, or about 96 logical pixels per inch |
To further formularise it, for android the coversion between logical pixel to dp can be:
$value_in_dp = 160 / 100 * $value_in_logical_point;
For iOS, it will be relatively more tricky:
$value_in_pt = ($ios_ppi/$scaling_factor) / 100 * $value_in_logical_point
Tips: Use MediaQueryData
for Scalable UI
MediaQuery class in flutter is used as a very important tool for developers to get information like device orientation, customized text scale, screen occlusion and user’s animation preferences. For iOS development, to use logic pixel to directly measure and render the layout will cause greate miss alignment in UI display across different device types. Take the following example below:
Container(
height: 40,
width: 60,
),
The above code will cause the following render difference:
So solve it, there are two possible solutions. The first is to convert the UI display unit of flutter which is the logical pixel to the iOS’s display unit point
(Can refer to the previus tips: ‘Conversion between units of UI measurement’ on how to do it). Another more simple way, is to scale the layout based on the size of the screen. Refer to the implementation below:
class SizeConfig {
static MediaQueryData _mediaQueryData;
static double screenWidth;
static double screenHeight;
static double blockSizeHorizontal;
static double blockSizeVertical;
void init(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData.size.width;
screenHeight = _mediaQueryData.size.height;
blockSizeHorizontal = screenWidth / 100;
blockSizeVertical = screenHeight / 100;
}
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: SizeConfig.blockSizeVertical * 20,
width: SizeConfig.blockSizeHorizontal * 50,
color: Colors.orange,
),
);
}
As a result, the layout between different iOS devices are scaled propotionally to the screen:
Tips: Use ‘Future’ for IO-bound operations.
Unlike in Java, Kotlin or Objc/Swift, by default, most of IO-bound operation are handled to sperated threads to have the task execution “off” the “main thread” with varies techniques. Without creating new threads, Dart executes IO-bound operations via ‘Future’. For operations wrapped in future, the call get appended to the end of event queue of the default isolates instance to ensure the current tasks on event queue such as UI render and user inputs does not get blocked. Take the following code as an example, the sample code and terminal output are demonstrated below:
void executeFuture() async {
await new Future(() {
print("'executeFuture' execution");
return "execution requeue from 'executeFuture'";
}).then((m) async {
await new Future.delayed(const Duration(milliseconds: 10));
print("$m: 1");
return m;
}).then((m) async {
await new Future.delayed(const Duration(microseconds: 10));
print("$m: 2");
return m;
});
}
void testFuture() {
// execute function
print("'executeFuture' invoking");
executeFuture();
// continious event queue processing
var i = 0;
while (i < 3) {
var order = i;
print("'loop: ${i++}'");
print("task invoking 'loop: $order'");
new Future.delayed(const Duration(milliseconds: 10), () {
print("task execution 'loop: $order'");
});
}
}
Execution output:
'executeFuture' invoking
'loop: 0'
task invoking 'loop: 0'
'loop: 1'
task invoking 'loop: 1'
'loop: 2'
task invoking 'loop: 2'
'executeFuture' execution
task execution 'loop: 0'
task execution 'loop: 1'
task execution 'loop: 2'
execution requeue from 'executeFuture': 1
execution requeue from 'executeFuture': 2
Note: Besides of the event queue, a microtask queue is also provided for each isolate to execute tasks with higher priority:
The event queue will never gets executed as long as there are remaining tasks on microtask queue. Hence for putting intensive and IO blocking tasks into microtask queue is strictly prohibited. Please please refer to the next tip on recognize on those CPU bound operations which are strongly recommended to be processed on separate isolate instance.
Tips: Use ‘Isolate’ for CPU-bound Opertations
When handling CPU-bound operations, the previous tip of adopting ‘future’ dose not help at all and will potentially froze the app UI. In this case, an separated isolate instance is recommended to process these type of tasks since isolates can make use of multiple available cores and multiplex for single core devices. In practise, common CPU-bound operations include:
- matrix multiplication
- cryptography-related (such as signing, hashing, key generation)
- image/audio/video manipulation
- serialization/deserialization
- offline machine learning model computation
- compression (such as zlib)