16 Dart tips that every fluent developer should know (Section 3)

1. Do you know? Dart supports string multiplication.

This is a simple program that shows how to print China with string multiplication. Come on, Gansu hold on:

My hometown Gansu is ill now, but I believe he will get better!

void main() {
  for (var i = 1; i <= 5; i++) {
    print('Gansu hold on, beef noodles, come on!' * i);
// Output:
Gansu hold on, beef noodles, come on!
Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on!
Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on!
Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on!
Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on! Gansu hold on, beef noodles, come on!

Isn't it cool?, Yes, I believe Gansu can hold on!

You can use it to check how long strings fit into the Text widget:

Text('You've shouted come on many times:' * 5)

2. Do you need to execute multiple futures at the same time? Use Future.wait.

Consider this simulation API class, which tells us the latest number 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);

To execute all these futures simultaneously, use Future.wait. This requires a * * list or futures and returns a future of lists:

final api = CovidAPI();
final values = await Future.wait([
print(values); // [1000, 100, 10]

This is ideal when the futures are independent, and they don't need to execute sequentially.

3. Implement "call" methods in Dart class to make them callable like functions.

This is an example PasswordValidator class:

class PasswordValidator {
  bool call(String password) {
    return password.length > 10;

Because the method is called call, we can declare a class instance and use it as a method:

final validator = PasswordValidator();
// You can use it this way:
validator('Go China');
validator('Go China,Beef noodles, hold on');
// You don't need to use it like this:
validator.call('Go China,Beef noodles, hold on');

4. The callback needs to be called, but only if it is not empty? Use the "?. call()" syntax.

Suppose we have a custom widget class, which should call the callback when a specific event occurs:

class CustomDraggable extends StatelessWidget {
  const CustomDraggable({Key key, this.onDragCompleted}) : super(key: key);
  final VoidCallback? onDragCompleted;

  void _dragComplete() {
    // TODO: Implement me
  Widget build(BuildContext context) {/*...*/}

To invoke the callback, we can write the following code:

  void _dragComplete() {
    if (onDragCompleted != null) {

However, there is a simpler method (pay attention to use?):

  Future<void> _dragComplete() async {

5. Use anonymous functions and functions as parameters

In Dart, functions are first-class citizens and can be passed to other functions as parameters.

Here are some codes that define anonymous functions and assign them to sayHi variables:

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

void welcome(String Function(String) greet,
             String name) {

sayHi is then passed to a welcome Function that takes a Function parameter and uses it to greet the user.

String Function(String) is a function type. It accepts a string parameter and returns a string. Because the above anonymous function has the same signature, it can be passed directly as a parameter or sayHi through a variable.

This coding style is common when using function operators such as map, where and reduce.

For example, this is a simple function that calculates the square of a number:

int square(int value) {
  // Just a simple example
  // It may be a complex function with a lot of code
  return value * value;

Given a list of values, we can map:

const values = [1, 2, 3];


Here we pass square as a parameter because its signature is exactly what the map operator expects. This means that we do not need to extend it with anonymous functions:

values.map((value) => square(value)).toList();

6. You can use collection if and spreads with lists, sets AND maps

Collection if and spreads are useful when you write the UI as code.

But did you know that you can also use them with maps?

Consider this example:

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

Here we declare a restaurantmaps, adding only avgRating and numRatings key value pairs if addRatings is true. Because we want to add multiple key value pairs, we need to use the extension operator (...).

7. Do you need to traverse the map in an empty and safe way? Use. entries:

Suppose you have a map:

const timeSpent = <String, double>{
  'Blogging': 10.5,
  'YouTube': 30.5,
  'Courses': 75.2,

Here's how to write a loop to run some code with all key value pairs:

for (var entry in timeSpent.entries) {
  // do something with keys and values
  print('${entry.key}: ${entry.value}');

By iterating over the entries variable, you can access all key value pairs in an empty and safe manner.

This is more concise and less error prone than this:

for (var key in timeSpent.keys) {
  final value = timeSpent[key]!;
  print('$key: $value');

The code above! You need to use the assertion operator () when reading the value, because Dart cannot guarantee the existence of the value of the given key.

8. Use named constructors and initialization lists to get a more ergonomic API.

Suppose you want to declare a class that represents a temperature value.

You can make your class API explicitly support two constructors named Celsius and Fahrenheit:

class Temperature {
  Temperature.fahrenheit(double fahrenheit)
    : celsius = (fahrenheit - 32) / 1.8;
  double celsius;

This class only needs a storage variable to represent the temperature, and uses the initialization list to convert Fahrenheit temperature to Celsius temperature.

This means that you can declare the temperature value like this:

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

9. getter and setter

In the class above Temperature, celsius is declared as a storage variable.

However, users may prefer to get or set the temperature in Fahrenheit.

This can be easily done using getter s and setter s, which allow you to define calculated variables. This is an updated course:

class Temperature {
  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;

This makes it easy to get or set the temperature using degrees Fahrenheit or Celsius:

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

Note: use named constructors, getter s, and setter s to improve class design.

10. Underline unused function parameters

In fluent, we often use widgets with function parameters. A common example is ListView.builder:

class MyListView extends StatelessWidget {
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (context, index) => ListTile(
        title: Text('all the same'),
      itemCount: 10,

In this case, we do not use the (context, index) parameter itemBuilder. So we can replace them with underscores:

  itemBuilder: (_, __) => ListTile(
    title: Text('all the same'),
  itemCount: 10,

Note: these two parameters are different (and), because they are separate identifiers.

11. Need a class that can only be instantiated once (another example)? Use static instance variables with private constructors.

The most important feature of a singleton is that there can only be one instance of it in the whole program. This is useful for modeling file systems and so on.

// file_system.dart
class FileSystem {
  static final instance = FileSystem._();

To create a singleton in Dart, you can declare a named constructor and use_ The syntax makes it private.

You can then use it to create a static final instance of the class.

Therefore, any code in other files can only access this class through the instance variable:

//. dart for other files
final fs = FileSystem.instance;
// Do something with fs

Note: final can cause many problems if you are not careful. Before using them, make sure you understand their disadvantages.

12. Need to collect unique data? Use collections instead of lists.

The most commonly used collection type in Dart is List

However, the list can have duplicate items, which is sometimes not what we want:

const citiesList = [

We can use a when a Set of unique values is required (please note that final is used):

// set is final, compiles
final citiesSet = {

The above code generates a warning because it occurs twice in Shenzhen. If we try to perform the same operation on constset, we will receive an error and our code cannot be compiled:

// set is const, doesn't compile
const citiesSet = {
  'Shenzhen', // Two elements in a constant set literal can't be equal

We can obtain useful API s, such as union, difference and intersection:

citiesSet.union({'Delhi', 'Moscow'});
citiesSet.difference({'London', 'Madrid'});
citiesSet.intersection({'London', 'Berlin'});

Bottom line: when you create a collection, ask yourself if you want its items to be unique and consider using a collection.

13. How to use try, on, catch, rethrow, finally

try and catch are ideal when using Future based APIs. If there is a problem, these APIs may throw exceptions.

This is a complete example of how to make the most of them:

Future<void> printWeather() async {
  try {
    final api = WeatherApiClient();
    final weather = await api.getWeather('London');
  } on SocketException catch (_) {
    print('Could not fetch data. Check your connection.');
  } on WeatherApiException catch (e) {
  } catch (e, st) {
    print('Error: $e\nStack trace: $st');
  } finally {

Some precautions:

  • You can add multiple on clauses to handle different types of exceptions.
  • You can use the fallback catch clause to handle all exceptions that do not match any of the above types.
  • You can use the rethrow statement to throw the current exception up the call stack while preserving the stack trace.
  • You can use finally to run some code after Future is completed, whether it succeeds or fails.

If you are using or designing some Future based API s, be sure to handle exceptions as needed.

14. Common Future constructors

The DartFuture class comes with some convenient factory constructors: Future.delayed,Future.value, and Future.error.

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

await Future.delayed(Duration(seconds: 2), () => 'Latte');

But sometimes we want to create an immediate Future:

await Future.value('Cappuccino');
await Future.error(Exception('Out of milk'));

We can use Future.value to complete successfully, or Future.error to complete with an error.

You can use these constructors to simulate responses from Future based API s. This is useful when writing mock classes in your test code.

15. Universal flow constructor

The Stream class also comes with some convenient constructors. The following are the most common:

Stream.fromIterable([1, 2, 3]);
Stream.error(Exception('something went wrong'));
Stream.fromFuture(Future.delayed(Duration(seconds: 1), () => 42));
Stream.periodic(Duration(seconds: 1), (index) => index);
  • Used to create a Stream from the value list Stream.fromiteratable.
  • Use Stream.value if you have only one value.
  • Used to create an empty stream from Stream.empty.
  • Use Stream.error to create a stream containing an error value.
  • Use Stream.fromFuture to create a stream that contains only one value that will be available when it is completed in the future.
  • Use Stream.periodic to create a periodic stream of events. You can specify a Duration as the time interval between events and an anonymous function to generate each value given its index in the stream.

16. Synchronous and asynchronous generators

In Dart, we can define the synchronization generator as a function that returns iteratable:

Iterable<int> count(int n) sync* {
  for (var i = 1; i <= n; i++) {
    yield i;

This uses sync * syntax. Inside the function, we can "generate" or yield multiple values. These will be returned when the iteratable function completes.

On the other hand, the asynchronous generator is a function Stream that returns a:

Stream<int> countStream(int n) async* {
  for (var i = 1; i <= n; i++) {
    yield i;

This uses this async * syntax. Inside the function, we can take the value of yield as in the case of synchronization.

However, if we like, we can use await's Future based API because it is an asynchronous generator:

Stream<int> 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;

So today's sharing will say goodbye to you. I'll see you tomorrow. Tomorrow will continue to bring you the wonderful content of fluent.

Posted by seby on Tue, 30 Nov 2021 05:58:37 -0800