16 Dart tips every Flutter developer needs to know

  1. Did you know that Dart supports string multiplication?
    The following example shows how to print a Christmas tree using string multiplication
void main() {<!-- -->
  for (var i = 1; i <= 5; i + + ) {<!-- -->
    print('' * i);
  }
}
//Output:
// 
//
// 
//
//

copy
Isn’t it cool?

You can use this to check whether the length of text in a Text widget is appropriate.

Text(You have pushed the button this many times:’ * 5)
copy
2. Need to execute multiple Futures at the same time? Use Future.wait.
Consider this mock API class that tells us the latest numbers of COVID cases:

//Mock API class
class CovidAPI {<!-- -->
  Future<int> getCases() => Future.value(1000);
  Future<int> getRecovered() => Future.value(100);
  Future<int> getDeaths() => Future.value(10);
}

copy
To execute these futures simultaneously, use Future.wait. The parameter requires a list of “futures” and will return a “list” of “futures”:

final api = CovidAPI();
final values =await Future.wait([
api.getCases(),
api.getRecovered(),
api.getDeaths(),
]);
print(values); // [1000, 100, 10]
copy
This is ideal when Futures are “independent” and do not need to be executed “sequentially”.

  1. We can implement a “call” method in a Dart class, so that we can call the class like a method.
    Here is a PasswordValidator class:
class PasswordValidator {<!-- -->
  bool call(String password) {<!-- -->
    return password.length > 10;
  }
}

copy
We define a call method, and then define an instance of the class to use it like a function:

final validator = PasswordValidator();
// can use it like this:
validator(test’);
validator(test1234’);
// no need to use it like this:
validator.call(not-so-frozen-arctic’);
copy
4. Need to call the callback method, but only if the callback method is not empty? Use “?.call()” syntax.
In the following example we define a widget and call the onDragCompleted callback when the event is triggered:

class CustomDraggableextends StatelessWidget {<!-- -->
  const CustomDraggable({<!-- -->Key key, this.onDragCompleted}) : super(key: key);
final VoidCallback? onDragCompleted;

  void _dragComplete() {<!-- -->
    // TODO: Implement me
  }
  @override
  Widget build(BuildContext context) {<!-- -->/*...*/}
}

copy
In order to call the callback function, we may have to write the following code:

void _dragComplete() {
if (onDragCompleted != null) {
onDragCompleted();
}
}
copy
But we can use a simple syntax like this (using ?.):

Future _dragComplete()async {
onDragCompleted?.call();
}
copy
5. Using anonymous functions and functions as parameters
In Dart, functions are first-class citizens and can be used as arguments to other functions.

The following demonstrates defining an anonymous function and assigning it to the sayHi variable:

void main() {
final sayHi = (name) => Hi, $name’;
welcome(sayHi, Andrea’);
}

void welcome(String Function(String) greet, String name) {
print(greet(name));
print(Welcome to this course’);
}
copy
Pass sayHi as a variable to the greet parameter of the welcome method.

String Function(String) is a function “type” that takes String parameters and returns String type. Because the above anonymous functions have the same “signature”, they can be passed directly as parameters.

This code style is very common when performing list operations such as map, where, and reduce.

As an example of calculating the square of a number:

int square(int value) {
// just a simple example
// could be a complex function with a lot of code
return value * value;
}
copy
Find all squares of an array:

const values = [1, 2, 3];

values.map(square).toList();
copy
Square is used as a parameter here because the signature of square is in line with the expectations of the map operation. So we can not use the following anonymous function:

values.map((value) => square(value)).toList();
copy
6. You can use collection-if and spreads with lists, sets, and maps
Collection-if and spreads are very useful when we write code.

These can actually be used with maps as well.

Look at the following example:

const addRatings = true;
const restaurant = {<!-- -->
  'name' : 'Pizza Mario',
  'cuisine': 'Italian',
  if (addRatings) ...{<!-- -->
    'avgRating': 4.3,
    'numRatings': 5,
  }
};

copy
We define a map of restaurants, and avgRating and numRatings will be added only when addRatings is true. Because there is more than one key-value, the spread operator (…) needs to be used.

  1. How to iterate over the entire map in a null-safe way? Use .entries:
    Look at an example below:

const timeSpent = {
Blogging’: 10.5,
YouTube’: 30.5,
Courses’: 75.2,
};
copy
The following is a loop through key-value:

for (var entry in timeSpent.entries) {
// do something with keys and values
print(${entry.key}: ${entry.value}’);
}
copy
8. Use named constructors and initialization lists to make the API more concise.
For example, we want to define a temperature class.

Our class needs to support “two” named constructors for Celsius and Fahrenheit:

class Temperature {
Temperature.celsius(this.celsius);
Temperature.fahrenheit(double fahrenheit)
: celsius = (fahrenheit – 32) / 1.8;
double celsius;
}
copy
The class only requires a variable to represent the temperature and uses an initializer list to convert Fahrenheit to Celsius.

We can use it like this:

final temp1 = Temperature.celsius(30);
final temp2 = Temperature.fahrenheit(90);

copy
9. Getters and setters
In the Temperature class above, celsius is used to represent temperature.

But some users may prefer Fahrenheit.

We can easily implement it through getters and setters, defining computed variables (if you have studied Vue, it will feel familiar). Continue to look at the following example:

class Temperature {
Temperature.celsius(this.celsius);
Temperature.fahrenheit(double fahrenheit)
: celsius = (fahrenheit – 32) / 1.8;
double celsius;
double get fahrenheit
=> celsius * 1.8 + 32;
set fahrenheit(double fahrenheit)
=> celsius = (fahrenheit – 32) / 1.8;
}
copy
Now it’s easy to use Fahrenheit or Celsius:

final temp1 = Temperature.celsius(30);
print(temp1.fahrenheit);
final temp2 = Temperature.fahrenheit(90);
temp2.celsius = 28;
copy
Tip: Use named constructors, getters, and setters to improve your class design.

  1. Unused parameters are underlined
    Take a common example ListView.builder:

class MyListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(all the same’),
),
itemCount: 10,
);
}
}
copy
In the above example we did not use the itemBuilder parameters (context, index). So we can use underscore instead.

ListView.builder(
itemBuilder: (_, _) => ListTile(
title: Text(all the same’),
),
itemCount: 10,
)
copy
Note*: These two parameters are different (
and __), they are “separate identifiers.”*

  1. A class only needs to be initialized once (singleton pattern)? Use static instance variables with a private constructor.
    The most important property of a singleton is that there can only be “one instance” of it in your entire program. This is useful to model things like a file system.

// file_system.dart
class FileSystem {
FileSystem.();
static final instance = FileSystem.
();
}
copy
To create a singleton in Dart, you declare a named constructor and make it private using the _ syntax.

Then define a final type static instance of the class.

From now on, this class can only be accessed through the instance variable.

// some_other_file.dart
final fs = FileSystem.instance;
// do something with fs
copy
Note: Singletons can cause a lot of problems if you’re not careful. Make sure you understand their disadvantages before using them.

  1. Want every item in a collection to be unique? Use a Set instead of a List.
    The most common collection in Dart is List.

Lists can have duplicate items, and sometimes we want the elements to be unique:

const citiesList = [
London’,
Paris’,
Rome’,
London’,
];
copy
At this time we need to use Set (note that we use final):

// set is final, compiles
final citiesSet = {
London’,
Paris’,
Rome’,
London’, // Two elements in a set literal shouldn’t be equal
};
copy
The above code will generate a warning because London contains two. If the set is defined as const, the code will generate an error and fail to compile successfully:

// set is const, doesn’t compile
const citiesSet = {
London’,
Paris’,
Rome’,
London’, // Two elements in a constant set literal can’t be equal
};
copy
When we use set, we can use APIs such as union, difference, and intersectio

citiesSet.union({Delhi’, Moscow’});
citiesSet.difference({London’, Madrid’});
citiesSet.intersection({London’, Berlin’});
copy
13. How to use try, on, catch, rethrow, finally
try and catch are very useful when we use Future based API.

Take a look at the following example:

Future printWeather() async {
try {
final api = WeatherApiClient();
final weather = await api.getWeather(London’);
print(weather);
} on SocketException catch (_) {
print(Could not fetch data. Check your connection.’);
} on WeatherApiException catch (e) {
print(e.message);
} catch (e, st) {
print(Error: $e\\
Stack trace: $st’);
rethrow;
} finally {
print(Done’);
}
}
copy
The following points need to be focused on:

Multiple ons can be added to catch different types of exceptions.
Finally, you can add a catch to catch exceptions that are not handled above.
Use the rethrow statement to throw the current exception out of the call stack “while retaining the stack trace.”
Use finally to run some code after the Future completes, whether it succeeds or fails.
When using Future-related APIs, be sure to ensure exception handling

  1. Some common constructors of Future
    There are some convenient constructors in Future: Future.delayed, Future.value and Future.error.

We can use Future.delayed to create a certain delay. The second parameter is an (optional) anonymous function that can be used to complete a value or throw an error:

await Future.delayed(Duration(seconds: 2), () => Latte’);
copy
Sometimes we can create a Future and return it immediately, which is very useful when testing mock data:

await Future.value(Cappuccino’);
await Future.error(Exception(Out of milk’));
copy
We can use a value of Future.value to indicate successful completion, or Future.error to indicate an error.

  1. Common Stream constructors
    The Stream class also comes with some convenience constructors. The following are the most common:

Stream.fromIterable([1, 2, 3]);
Stream.value(10);
Stream.empty();
Stream.error(Exception(something went wrong’));
Stream.fromFuture(Future.delayed(Duration(seconds: 1), () => 42));
Stream.periodic(Duration(seconds: 1), (index) => index);
copy
Use Stream.fromIterable to create a Stream from a list.
Created from a single value using Stream.value.
Use Stream.empty to create an empty stream.
Use Stream.error to contain the error value.
Use Stream.fromFuture to create a stream that contains only one value, and that value will be available when the future completes.
Use Stream.periodic to create a periodic stream of events.
16. Sync and Async Generators
We can define a “synchronous” generator whose return type is Iterable:

Iterable count(int n) sync* {
for (var i = 1; i <= n; i + + ) {
yield i;
}
}
copy
The sync* syntax is used here. Inside the function, we can yield multiple values. These values will be returned as an Iterable when the function completes.

In addition, an “asynchronous” generator needs to use a Stream as the return value

Stream countStream(int n)async* {
for (var i = 1; i <= n; i + + ) {
yield i;
}
}
copy
The async* syntax is used here. Inside the function, we can yield multiple return values.

In the “asynchronous” generator we can also use Future-related functions:

Stream countStream(int n)async* {
for (var i = 1; i <= n; i + + ) {
// dummy delay – this could be a network request
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
copy
at last
I hope you enjoy these tips and use them to improve the code in your Flutter applications. You can leave a message and tell me which tip is your favorite! !