Try   HackMD

Common Dart & Flutter Issues

A collection of common issues to cover whenever reviewing Dart or Flutter code. These comments follow the Conventional Comments structure.

Issues

Problems that must be addressed.

initState first

issue: Avoid implementing this method before calling super.initState().
suggestion: Call super.initState before any other logic in your Stateful Widget.

Implementations of this method should start with a call to the inherited method, as in super.initState().

dispose last

issue: Avoid calling super.dispose() before implementing this method.
suggestion: Call super.dispose after any other logic in your Stateful Widget.

Implementations of this method should end with a call to the inherited method, as in super.dispose().

Strong typing

issue: Avoid using dynamic type where possible.
suggestion: Consider using stronger typing rather than relying on dynamic

This language feature offers the following benefits:

  • Revealing type-related bugs at compile time. A sound type system forces code to be unambiguous about its types, so type-related bugs that might be tricky to find at runtime are revealed at compile time.
  • More readable code. Code is easier to read because you can rely on a value actually having the specified type. In sound Dart, types can’t lie.
  • More maintainable code. With a sound type system, when you change one piece of code, the type system can warn you about the other pieces of code that just broke.
  • Better ahead of time (AOT) compilation. While AOT compilation is possible without types, the generated code is much less efficient.

See also:

https://dart.dev/guides/language/type-system

Const keyword

issue: Avoid using var/final when possible.
suggestion: Consider using const over var/final.

The const keyword serves as an indicator that tells the compiler that, “for this variable, it will never change in the lifetime of this code, so create just one copy of it and where ever it is mentioned, just reference back to the one copy you already created and don’t create a new one.”

See also:

https://www.youtube.com/watch?v=DCKaFaU4jdk

https://medium.com/flutter-community/the-flutter-const-keyword-demystified-c8d2a2609a80

https://resocoder.com/2020/01/06/dart-const-tutorial-all-you-need-to-know-const-expressions-canonical-instances-and-more/

Functions vs. Widgets

issue: Avoid returning widgets from helper methods.
suggestion: Prefer to either inline widgets or extract them into another widget.

Extracting widgets to a method is considered a Flutter anti-pattern, because when Flutter rebuilds the widget tree, it calls the function all the time, making more processor time for the operations, and potentially short-circuiting hot reload.

There are a number of advantages to extracting UI as a widget rather than using a helper method:

  • Allows the framework to do performance optimizations (const constructor, more granular rebuild, etc.)
  • Ensures that layout switching properly disposes of resources where functions may reuse previous state.
  • Ensures that hot-reload works properly
  • Integrates with the DevTools widget inspector
  • Better error messaging, key definitions, and use of context.

It is also a good practice to create private widgets for UI that you would not like to expose in other places in the application. Or you could expose the widget for tests using the @visibleForTesting decorator. That way you can test the component but if you try to use it in the application, you will see a warning.

See also:

https://flutter.dev/docs/perf/rendering/best-practices#controlling-build-cost

https://www.youtube.com/watch?v=IOyq-eTRhvo

https://stackoverflow.com/a/53234826

Suggestions

Non-blocking improvements.

DI/IOC

suggestion: Consider passing dependencies in constructor rather than instantiating them in the class.

Example:

class NetworkHelper { NetworkHelper( this.url, { http.Client? client, }) { this.client = client ?? http.Client(); } late final http.Client client; final Uri url; Future getData() async { http.Response response = await client.get(url); if (response.statusCode == 200) { String res = response.body; final data = jsonDecode(res).cast<Map<String, dynamic>>(); return data; } else { print(response.statusCode); } } ///...rest of class }

Passing in the client as a dependency will allow you to easily mock the client when writing tests.

See also:

https://flutter.dev/docs/cookbook/testing/unit/mocking

suggestion: Consider avoiding print calls in production.

See also:

https://dart-lang.github.io/linter/lints/avoid_print.html

Environment configuration

suggestion: Consider setting environment variables (such as URLs) using an environment configuration.

See also:

https://stacksecrets.com/flutter/environment-configuration-in-flutter-app#Introduction_To_Environment_Configuration

Public vs. Private

suggestion: Consider making variables private using an underscore and improve readability.

A public member can be accessed from outside the class. That means that if something goes wrong with a public field, the culprit could be anywhere. Additionally, using underscores for private variables improves the readability of your code by allowing users to differentiate private class variables from function variables.

See also:

https://softwareengineering.stackexchange.com/questions/143736/why-do-we-need-private-variables

Using shrinkwrap

suggestion: Avoid using ListView.shrinkWrap. Consider instead using a CustomScrollView + SliverList

Example:

CustomScrollView( slivers: [ SliverList( delegate: SliverChildBuilderDelegate( (context, index) => Container(), childCount: someObject.length, ), ), ], ),

According to the Flutter documentation, using shrinkWrap in lists is expensive performance-wise and should be avoided, since using slivers is significantly cheaper and achieves the same or even better results.

See also:

https://github.com/dart-lang/linter/issues/3496

Using Border.all

suggestion: Prefer using Border.fromBorderSide over Border.all for performance, since the former is a const constructor

Example:

class BorderWidget extends StatelessWidget { final Widget child; const BorderWidget({ Key? key, required this.child, }) : super(key: key); @override Widget build(BuildContext context) { return Container( border: const Border.fromBorderSide(BorderSide( color: const Color(0xFF000000), width: 1.0, style: BorderStyle.solid, )), child: child, ); } }