Boost your Flutter app with the newest Dart version

Developer tips for writing high-quality Dart code in Flutter

Mobile@Exxeta
Flutter Community
6 min readSep 16, 2024

--

Photo by Pixabay from Pexels

In the ever-evolving world of development with flutter, writing high-quality Dart code is essential for creating efficient and robust applications. With the current version of Dart 3.4.4, developers have the possibility to use cool features and enhancements which can significantly improve code quality and performance. In this article, we will explore three expert tips to boost your Flutter app by using the latest Dart SDK.

Enum constructors

Enums in Dart provide a way to define a fixed set of constant values. Prior to Dart 2.17, adding associated values like strings or objects to an enum required using getter methods and switch statements. However, with Dart 2.17 and later, enum constructors allow developers to get rid of long enum declarations. Let’s jump into two approaches!

Using enums without constructors

Default usage of enums with constructors is maintained, like you can see in the following code snippet. In this example, the name and locale properties are derived using getter methods and switch statements.

enum Langauge { 
german,
english,
french,
spanish;

String get name => switch (this) {
german => 'Deutsch',
english => 'English',
french => 'Français',
spanish => 'Español',
};

Locale get locale => switch (this) {
german => const Locale('de', 'DE'),
english => const Locale('en', 'US'),
french => const Locale('fr', 'FR'),
spanish => const Locale('es', 'ES'),
};
}

While functional, it is not as clean as it could be… but Dart has a solution for us developers!

Using enums with constructors

With the introduction of enum constructors, you can directly associate values with each enum constant. This makes the code more concise and easier to understand.

enum Language { 
german('Deutsch', Locale('de', 'DE')),
english('English', Locale('en', 'US')),
spanish('Español', Locale('es', 'ES')),
french('Français', Locale('fr', 'FR'));

const Language(this.name, this.locale);
final String name;
final Locale locale;
}

Try it yourself:

In this example, each enum value is directly associated with its corresponding name and locale. The constructor is used to initialize these properties, making the code more maintainable for additional fields.

Benefits of Enum Constructors:

  • Readability: The association between enum values and their data is explicit and easier to read.
  • Maintainability: Adding or modifying enum values is straightforward, as all related data is in one place.
  • Less boilerplate: Reduces the need for repetitive switch statements and getter method.

Enhancing your Dart code with extension types

Dart 3 introduces extension types, a powerful feature that allows you to extend existing types instead of needing boilerplate wrapper classes. This makes your code cleaner and more efficient. Let’s compare two approaches to illustrate the benefits for better understanding how extension types improve the performance.

Using a Wrapper Class

Traditionally, to extend the functionality of an existing object, you would create a wrapper class. This approach can make the codebase more complex.

class PointWrapper { 
PointWrapper(this.point);
final Point point;

double calcDistanceTo(Point other) {
final dx = point.x - other.x;
final dy = point.y - other.y;
return sqrt(dx * dx + dy * dy);
}
}

In this example, the PointWrapper class is created to add different calculation functions, like the calcDistanceTo method, for a Point class. The usage of the Wrapper looks like:

const pointA = Point(2, 3); 
const pointB = Point(2, 3);
final wrapper = PointWrapper(pointA);
final distance = wrapper.calcDistanceTo(pointB);

Here we can see two points are instantiated. On one hand to use pointA for the PointWrapper instance and on the other hand pointB for the reference to calculate the distance between both points.

Using Extension Types

Extension types allow you to directly extend existing types with additional functionality, resulting in cleaner and more concise code.

extension type PointWrapper(Point point) { 
double calcDistanceTo(Point other) {
final dx = point.x - other.x;
final dy = point.y - other.y;
return sqrt(dx * dx + dy * dy);
}
}

Try it yourself:

Benefit of extension types:

“Extension types serve the same purpose as wrapper classes, but don’t require the creation of an extra run-time object, which can get expensive when you need to wrap lots of objects. Because extension types are static-only and compiled away at run time, they are essentially zero cost.” — Dart

Asynchronous executions with Dart Isolates

Dart’s Isolates (introduced with Dart version 2.4) provide a powerful way to run code in parallel, while offloading heavy computations to separate threads. This ensures your main thread remains responsive, especially important in Flutter applications for better performance. Let’s compare two approaches to illustrate the benefits.

Asynchronous Execution Without Isolates

Traditionally, you might use async and await to perform asynchronous operations. While this is effective, it doesn’t leverage multiple threads, which can still cause the main thread to wait for the operation to complete.

For example purpose, we use an asynchronous function to read a huge file from the file system and return the content with the following implementation:

Future<String> readFileAsync(String path) async { 
final file = File(path);
final content = await file.readAsString();
return content.trim();
}

In the following example, loadContent calls the async function to read the content of a file.

void loadContent() async { 
const path = 'lib/file.txt';
final content = await readFileAsync(path);
}

However, it still runs on the main thread, which can lead to performance issues if the operation is time-consuming.

Asynchronous Execution with Isolates

Isolates allow you to run Dart code in parallel. By using Isolates, you can offload heavy tasks to separate threads, ensuring the main thread remains responsive.

Flutter itself recommends that you should use Isolates whenever your application is handling computations that are large enough to temporarily block other computations. The most common example for Isolates in Flutter applications is the handling of those computations, that might otherwise cause the UI to become unresponsive.

From now on, with Dart, it is super easy to do this with just a minor change inside the code. We only need to call Isolate.run() and use our async function inside of it.

 void loadContent() async { 
const path = 'lib/file.txt';
final content = await Isolate.run(() => readFileAsync(path));
}

In this example, the loadContent function utilizes an Isolates to read the file. The Isolate.run() method runs the readFileAsync function in a separate thread, improving performance by not blocking the main thread. In addition, the newly created process has its own heap, which is completely different to the heap, which is used by the main thread.

Inside the Docs for Isolates it says there are not any rules about when you must use isolates, but they list some situations where they can be useful:

  • Parsing and decoding exceptionally large JSON blobs.
  • Processing and compressing photos, audio, and video.
  • Converting audio and video files.
  • Performing complex searching and filtering on large lists or within file systems.
  • Performing I/O, such as communicating with a database.
  • Handling a large volume of network requests.

Side node:
If you’re using Flutter, you can use Flutter’s compute function instead of Isolate.run():

Future<R> compute<M, R>(ComputeCallback<M, R> callback, M message, {String? debugLabel})

Benefits of Using Isolates

  1. Improved Performance: Offload heavy computations to parallel threads, keeping the main thread responsive.
  2. Enhanced Responsiveness: Ensures smooth UI updates by preventing the main thread from being blocked.
  3. Better Resource Utilization: Leverages multicore processors for parallel execution of tasks.

Conclusion

In summary, the introduction of enum constructors, extension types, and Isolates in Dart brings significant enhancements to the language, enabling developers to write more efficient, readable, and maintainable code. These improvements collectively enhance the developer experience, allowing you to build more robust and performant applications with greater ease. Embrace these changes to elevate your Dart and Flutter development practices to the next level. What do you think of the new features, and have you made use of it yet? (By Tobias Rump)

--

--

Mobile@Exxeta
Flutter Community

Passionate people @ Exxeta. Various topics around building great solutions for mobile devices. We enjoy: creating | sharing | exchanging. mobile@exxeta.com