Ever been scared of pressing that CI "release" button ? Do the words "bugs" and "regressions" ring any bell ?
It's time to leave the fear behind and start delivering higher-quality apps !
In this article I will try to show you why code quality is important and how to improve it in your Flutter app through various tools and techniques. Hopefully by the end of this article you will have the keys to deliver your apps with more confidence 🚀
You said code quality ? 🤔
Before jumping right to the solutions, I think it's important to do a small reminder of what code quality is, and why it is (very very very) important.
Code quality is a metric that usually tell whether a given code is good or bad. Obviously this is very subjective and differ from one project to another but it doesn't really matter because the goals behind code quality are almost always the same :
Increasing confidence & reputation 👍
Delivering a software with confidence is extremely important because that way you do not fear new releases and as you deliver with more confidence, you can deliver more often, making your delivery cycle a virtuous circle. And as developers we are always happy not having to work on the week-end because the latest release was full of bugs. 🦟 🪲
Less risk, more fun.
Obviously, your company reputation is also at stake here : the more perceived software quality (fast, secure, reliable, etc) your users get, the more confidence they will have to trust you and keep using your app (and even recommend it to others).
Increasing code reuse ⌨️
Unless you are developing a one-time POC (and even then, beware the "POC to production" effect !), you will spend a lot of time developing and maintaining your app : it could be weeks, months or even years ! The more quality you put at first, the less cost you will have to add further developments in the future.
In the other hand, low quality code would not only make you pay more in the long term but could really damage your company business as any change to the app could get really tricky and will potentially freeze your project.
What metrics make for a good code quality app ? 📊
I won't go into details here because other articles already describe this, but basically we can find :
- Reliability. How probable our app will run without unexpected failures.
- Maintainability, extensibility and reusability. How easy it is to add new code around existing code or change existing code.
- Testability. How easy it is to test the app.
- Portability. How well your app will behave in different environments. For a Flutter mobile app you should make sure that your app is running as well on Android and on iOS. For a web or desktop app, you also might want to test multiple browsers or OS platforms.
Code quality in Flutter 📱
Code quality in Flutter is no different from code quality in any other language, framework or anything else in general. Linters, tests, CI, reviews, refactoring, coding conventions, code quality measurement tools...
Fortunately for us, Flutter has them all !
I will now show a few tools that can be used to improve, guarantee and measure our code quality in Flutter. Note that my list might not be exhaustive (please add your tools in the comments !), and is in no particular order. Finally, it is important to understand a few things when talking about code quality :
- Good/experimented developers tend to write better quality code. Do not rely solely on tools below to claim that your code is good, rather think about the code you are writing and try to write the best code you can. Tools are only here to help you, but they won't think for you. This is particularly true for business matters where tools can hardly verify what's actually expected.
- You should not pick a single tool and think that it will be enough to have a good code quality. Many tools should be used together to incrementally improve code quality and measure it. And in the end, do not forget that tools can only do what you want them to do and their usefulness depends on what you do with them. As an example, it's good to have tests but if no one in the team ever look at whether they are succeeding or failing, they are pretty much useless.
Linters ℹ️
Linters are simple code analysis programs that are able to detect common programming errors and syntax issues. The way linters work is by defining a set of rules, that can usually be configured, and those rules will be checked by a command line or by the IDE every time a new code is added/updated/removed.
The good news is that since Flutter 2.10, the package flutter_lints is added by default to your pubspec.yaml
when you create a new app with Flutter command line flutter create
:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.1
With that, you also get a file called analysis_options.yaml
at the root of your project that will include the source package :
include: package:flutter_lints/flutter.yaml
linter:
rules:
So what do we have here ? Pretty easy, we are just saying that we want to use pre-configured set of rules from flutter_lints package. Joy of open source, you can jump to the actual repository to check what rules are defined.
Now you have to keep in mind that this set of rules has been defined by the Flutter team as the minimal set of recommended rules. Though we can trust the team that developed the framework to give us good rules, you can always customize this and deactivate or activate individual rules.
Say I want to enforce the declaration of return types and trailing commas, I just have to update my analysis_options.yaml
file :
include: package:flutter_lints/flutter.yaml
linter:
rules:
always_declare_return_types: true
required_trailing_commas: true
And if you're curious what rules exists, the complete list is here ! In my opinion it's always good to take a look at the existing rules to understand how to better code in Dart and Flutter, and understand what to avoid.
In my case I usually customize a lot of rules to define code conventions that work for my team. I like to have linter rules by default in new apps but the default linter sets about 44% of rules which is quite lax, I rather have stronger code conventions so everyone in the team uses pretty much the same code, and is able to read others' code. I usually activate around 80% of rules.
If you want to learn more about Flutter linting including a comparison between various popular packages, you should definitely read this Mike Rydstorm article on Flutter Linting, and he even created a Google Sheet table to compare the linter packages rule by rule !
Finally, to take linters to the next level, have a look at Dart Code Metrics. This package takes linter rules a little bit further by checking additional rules, more complex anti-patterns or code metrics, and also by checking for unused files and l10n strings.
Oh and before I forget, if you are new to Dart language or feel like you need a refresher, you absolutely must have a look at Effective Dart guidelines.
Refactoring & IDE features 🧑💻
I usually spot a junior developer quickly on the first pair-programming or pair-review session with the way a developer uses its tools. I don't mean to offend anyone, I'm just saying modern IDEs have a lot of tools and features that allow quick and strong refactoring, so we should use and abuse them, that's why there are here for.
VSCode and AndroidStudio (probably used by 99.99% of Flutter developers nowadays) both include powerful tools to refactor. Whether you want to rename a field or a method, extract a code block to a method or inline a method into another, you probably can delegate this cumbersome task to your IDE:
And if you do this often, learn the shortcut and become more efficient in your everyday life !
But your favorite IDE has many other (not-so) hidden gems.
A good example is quickfix that will apply fixes to common mistakes / warnings :
And you know what ? The great thing is that your IDE is able to read your set of defined rules in your analyis_options
file and will then show you the errors / warnings / hints directly in concerned files. How great is that ?
Another feature that saved my Flutter-developer life when I started is the auto-completion feature. It is just crazy how fast you can create widgets, wrap widgets into others and do a lot of cool things. If you are currently adding widgets and counting how many closing parenthesis you have, just stop and use auto-completion :
You got it, IDEs are powerful and love to work for you, so help yourself and use them. I'll leave you on the IDE part with the auto-format feature, particularly used in Flutter if you don't want to go crazy :
Tests ✅ ❌
Yes, testing is essential to keep code quality high. It is useful in the design phase because for a component to be testable means that you need to design it correctly (in isolation, should not do too much, etc.). It is also very useful because it is, if done correctly, a repeatable step that can be automated to check for non-regressions as you add features and fix bugs.
I won't go into too much details on testing because the official documentation already contains a comprehensive chapter on testing.
The documentation differentiate three types of tests :
- Unit tests test a single function, method, or class.
- Widget tests (in other UI frameworks referred to as component test) test a single widget.
- Integration tests test a complete app or a large part of an app.
If you don't know the difference between those tests, please go read the doc, it is very important to know which does what.
However I feel that there is a fourth type of test that is often unknown to developers and yet very powerful in my opinion :
🏆 Golden tests 🏆
A golden test is just a specific kind of widget test except that we use Flutter test framework to actually check that our widget rendering is conform to what we except it to be. The widget rendering must match a pre-generated rendered image called a "golden file", hence the name of the test.
I won't go into details here because other articles already do, but basically what you need to remember is that golden tests are widget tests, which means they execute almost as fast as widgets tests, and much (much, much) faster than integration tests, and allow for quickly testing visual rendering of our widgets.
Here is an example where I generated a golden file for my tiles widget, on both light and dark themes (with a single golden test) :
In case anything goes wrong, failures images get generated and come in multiple variants, here in "maskedDiff" mode where images are superposed and only changes are color-highlighted (in pink below) :
Or an isolated diff where only the diff is shown :
I just wanted to show you a quick overview of golden tests, I personally love them and use them to quickly generate image variants for various parameters :
- Light and Dark themes
- Different orientations (e.g. portrait / landscape)
- Different devices sizes and pixel aspect ratio (e.g. iPhone 5 to iPhone 13)
ℹ️ Just a warning though, testing a lot of variants in each test will quickly increase the number of images being generated and the total execution time. Example :
4 devices sizes x 2 themes x 2 orientations = 16 tests (for a single test)
You're the master, you decide what you need to do.
Final advice on golden tests : have a look at Alchemist package to make your life golden ☀️. An alternative for goldens is the more known golden_toolkit package.
And just to sum up, here's a quick overview of all the tests types in Flutter :
Code coverage
First of all a disclaimer : I don't want to start a war on whether code coverage should be used as a measure (or the only measure) of code quality. If you rely solely on code coverage to measure code quality, you have probably a long way to go... code coverage in itself does not mean much as a 100% coverage of a piece of code does not the mean that the tests are good and actually test something.
That said, there are cases when code coverage can be interesting as part of a global code quality measurement, for example you might want to make sure that your code has a decent code coverage and does not get lower and lower as time passes (and code is added).
Good news, Flutter allows you to just do that with the simple command line argument --coverage
. However, to actually generate a coverage report from the raw result we need to use a combination of junit
and lcov
librairies. I usually use the following script in my projects:
#!/bin/bash
## Prepare coverage folder and JUnit report lib
mkdir -p coverage/
flutter pub global activate junitreport
## Run tests with coverage and JUnit report
flutter test --coverage coverage/ --machine | tojunit --output test_results.xml
## Generate coverage report
lcov --remove coverage/lcov.info \
"lib/main.dart" \
"lib/other_file_to_remove_from_coverage.dart"
-o coverage/lcov_cleaned.info
genhtml -o coverage/ coverage/lcov_cleaned.info
Once you execute this script, you should get a report in coverage/
folder that looks like this :
Here you go !
CI & CD (Continuous Integration & Continuous Deployment)
I have only one thing to say :
You must use a CI.
You must use a CI.
You must use a CI.
A good CI & CD platform(s) is essential to a successful project for many many reasons, here are a non-exhaustive list :
- It is a gain of time because you don't have to run each command manually, both for building, testing and deploying your apps
- It is automated, making builds and deploys repeatable.
- It allows for continuous verification of many variables (successful compilation, succeeding tests, etc) and allows for giving constant feedback to the dev team : are there any warnings for potential regressions ?
Anyway, you must use a CI & CD platform.
This is not really a Flutter-specific topic but I had to mention it. The only specificity with Flutter builds are related to iOS because iOS apps can only be built on a MacOS runner... yeah 🙌
Other than that, you can use pretty much any platform or combination of platforms (and I have used a lot on my various clients projects) as long as you can implement a decent build and a deploy (those steps can be separated though because releases are usually manual) : GitLab CI, Github Actions, Bamboo, Bitrise, Azure Devops, Codemagic...
By the way, I wrote an article about 2 years ago (might not be up-to-date now...) on how to deploy apps to the stores using Codemagic in combination with Fastlane.
Ideally you want to try and give as much as feedback to your fellow developers on each build : lint checks result, failed tests including golden failures, tests results, code coverage, etc. A very promising Dart package called Danger.dart does just that, and here is an article that explains the process a little bit more.
Code quality measurement
If you are using the tools presented in this article you should by now have pretty good measures of your app code quality. Linters, tests, code coverage, etc.
However, some people might still want some kind of dashboard to gather all code quality related measures into one place.
Ring any bell ? Don't be shy, I know that you are thinking of Sonarqube !
And luckily for us, there is now a Flutter / Dart package available to plug into Sonarqube for our loved apps analysis !
This library called sonar-flutter generates the well-known Sonarqube dashboard after taking into account all Dart & Flutter related measures :
Conclusion
In this article we saw that code quality is very important to make a Flutter app more safe, reliable and maintainable. We also had a look at a few tools that can make our life easier when trying to improve code quality in our apps.
Please note that there are many more techniques that I did not mention earlier such as pair-programming, code reviews, documentation or TDD. Because they are general techniques, I chose to focus on specific Dart or Flutter tools here to try and give you a good overview of what you can do. Obviously you can use any known technique in addition to the specific tools if you feel the need to :
Your team is the only master of your app quality !