Getting started: Creating your Flutter project
The optional not-so-optional steps that Flutter doesn't mention when creating your project.
Last time I announced that I will start a series of example. Today, let’s get started with this project by… creating the project.
This article won’t be about how to install Flutter + create a project. The official installation guide already does an excellent job at explaining how to do that.
What we will see instead are the extra steps that can benefit larger projects, that Flutter don’t necessarily mention.
Step 1: (optional) Use the /packages folder convention
Before even creating our project, let’s prepare our folders a bit.
Our goal is to make a medium/large scale application. Large applications almost always have code that can be extracted in separate packages/modules/plugins. For example, you may want to have a separate UI library.
One way to handle projects with multiple packages in Dart is through mono-repositories,
using the /packages
folder convention, which we will use for this project.
Opting for this folder architecture as soon as possible will avoid some difficulties
that you could face if done later, like merge conflicts.
This consists of having the following folder architecture:
.git/
.gitignore
packages/
my_project/
pubspec.yaml
lib/
...
my_first_package/
pubspec.yaml
...
For more about creating packages and folder architectures, we will have a dedicated article later.
Can’t I just put everything inside the same project?
You could (hence why this step is marked as optional).
We will use such architecture in this tutorial as it showcases how to deal with multiple packages at the same time (and how Dart handles this smoothly). But you don’t have to.
This is a small trade-off. It guarantees a proper separation of concerns and that code is easily extractable, at the cost of making the CI a bit more complex.
Arguably, it is safer to go with the /packages
architecture from the start.
If you encounter problems with it, reverting to having everything in one project
is straightforward.
On the other hand, splitting one monolithic project in packages can be very
tedious.
Step 2: Adding the analysis_options.yaml
file
Whether or not you oped-in for the /packages
architecture, you should have a
Flutter project ready (if not, refer to Getting started).
From there, one of the biggest addition that you could do to your project,
is adding a custom analysis_options.yaml
.
An analysis_options.yaml
file is a file that allows you to enable extra warnings
and errors for your project.
Flutter projects do come with a default (hidden) analysis_options.yaml
. But
the default behavior is very limited, and not appropriate for large projects.
This file is special in that it can be placed anywhere above your pubspec.yaml
file
(it doesn’t even have to be inside your project).
This is very useful for our /packages
folder architecture, as it allows sharing
that configuration file with all of our packages at once.
It is usually placed at the root of your repository like so:
.git/
.gitignore
analysis_options.yaml << Here
packages/
my_project/
my_first_package/
...
For what to put in that file, let’s go to the next steps.
Step 3: Disabling implicit-dynamic + implicit-cast
One of the biggest reason why we are creating an analysis_options.yaml
is to
disable two languages “features”:
implicit-dynamic
implicit-cast
This is done by adding the following to our analysis_options.yaml
:
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
Now, the reason why we would want to disable them is, these two features are residues of the old untyped Dart 1 and will cause code that is likely wrong to compile.
implicit-dynamic
The implicit-dynamic
feature is about type inference. If enabled, if the type-inference
fails, implicit-dynamic
will make the type fallback to dynamic
instead.
A common situation where this may hurt you is with lists. For example, you may write:
var someList = [];
// TODO: add some items
This looks innocent, but the problem with this code is that it is equivalent to:
List<dynamic> someList = [];
which means we can both add anything to the list:
var someList = [];
someList.add(42); // works
someList.add('hello world'); // works too
and do whatever we want with the items, even accessing properties that do not exist:
var someList = [];
someList.add(42);
for (final number in someList) {
print(number.somePropertyThatDoesNotExist); // Compiles but fails at runtime
}
By disabling implicit-dynamic
, this will make our list declaration not compile
anymore:
var someList = []; // Compile error: Missing type argument for list literal.
We can then fix it by explicitly specifying what the type of the list items is:
var someList = <int>[]; // a list of integers only
someList.add(42); // works
someList.add('hello world'); // Compile error, `String` is not an `int`
for (final number in someList) {
// Compile error, no property `somePropertyThatDoesNotExist` on `int`
print(number.somePropertyThatDoesNotExist);
}
If you truly want to have multiple types of items in your list, I would highly
suggest using something like Freezed, with its support for union-types.
This will prevent many mistakes from happening.
I will explain more about Freezed in a different article.
implicit-cast
The implicit-cast
feature is related to variable assignments. It makes using
the as
keyword optional for “upcasts”.
Similarly to implicit-dynamic
, this leads to potentially unsafe code, for the
sake of not writing a few characters.
A typical scenario where implicit-cast
will hurt you is when you have an
abstract class that has multiple concrete implementations.
An example is num
, the common interface between int
and double
.
If implicit-cast
is enabled on your project, then you will be able to write the
following absurd code:
num baseClass = 42; // we assigned an `int` to `num`, this is valid
// UNSAFE! We assigned `num` to `double`. This is valid with `implicit-cast` only.
// This will crash at runtime, as `baseClass` contains an `int` here.
double concreteClass = baseClass;
By disabling implicit-cast
, this will make the previous code fail to compile:
num baseClass = 42;
// Error: A value of type 'num' can't be assigned to a variable of type 'double'.
double concreteClass = baseClass;
We can then fix this compilation error either through a cast:
num baseClass = 42;
double concreteClass = baseClass as double;
which makes it explicit that we are doing something unsafe.
Or we can use the is
keyword:
num baseClass = 42;
double concreteClass = baseClass is double ? baseClass : 0.0;
which gracefully handles the scenario where our value is of the wrong type.
Step 4: Lint rules
Now that the most important is out of the way, we will want to enable some lint rules.
There is a ridiculous available amount of lint rules available, where the full list is available here: https://dart-lang.github.io/linter/lints/
For large projects, we may want to enable as many rules as possible, unless
conflicting with our team.
The awkward part is, because of the Dart legacy, some of these rules conflict with
others or don’t represent good practices anymore.
Similarly, this is a relatively opinionated topic. As such, there are no
finite list of rules to enable or not.
But there are a few options to simplify your decision. You can start from an existing list of rules, such as:
-
The rules used internally by Flutter
It is complete, although it deviates quite a bit from standard practices.
For example, for some time, Flutter usedimplicit-cast
and enabled theavoid_as
rule, which is the opposite of what we’ve done in step 3. -
pedantic, by Google
This list is the bare minimum that Google enables on its Dart projects.
It contains some of the most important rules, but also miss many interesting ones.
For example, it does not enable any of the Flutter-specific rules likeno_logic_in_create_state
. -
effective_dart, by Google
It tries to be a representation of the official Effective Dart Guidelines.
Same thing as with pedantic, it misses on the Flutter-specific rules. -
lint, by Pascal Welsch (GDE)
A community attempt at having a stricter equivalent to pedantic/effective_dart.
It enables a lot more rules than pedantic/effective_dart, and tries to solve the different conflicts.You may or may not agree with some of the enabled rules, but it is a good go-to. One interesting thing with this one is, it gives a good explanation every time a rule is disabled.
Alternatively, you can make your own list by enabling all rules (from here), and then disable rules one by one until it matches your needs.
Managing your lint rules easily
Whether you are making your own list or you’re starting from an existing one, you will quickly face one problem:
Readability.
There are 160+ rules, with some being added regularly. It can be very tedious to know what is enabled and what is not.
To fix this, I would recommend having two different files:
-
A
all_lint_rules.yaml
, with all rules enabled, without exception.
This is a local copy of the list of all rules. -
Your
analysis_options.yaml
, which imports thatall_lint_rules.yaml
file, and disable the rules you do not want.
This means that instead of:
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
linter:
rules:
- always_declare_return_types
- always_put_control_body_on_new_line
# Some explanation why we don't want this lint
# - always_put_required_named_parameters_first
... another 160 lines of rules
where you have both enabled and disabled rules mixed in one file,
you would do:
# Enable all rules by default
include: all_lint_rules.yaml
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
errors:
# Otherwise cause the import of all_lint_rules to warn because of some rules conflicts.
# The conflicts are fixed in this file instead, so we can safely ignore the warning.
included_file_warning: ignore
# Explicitly disable only the rules we do not want.
linter:
rules:
# Some explanation why we don't want this lint
always_put_required_named_parameters_first: false
This approach has two benefits:
-
We know in a single glance what is disabled, since our
analysis_options.yaml
no longer contains both enabled and disabled rules. -
It is easy to maintain our list when new rules are added. It is just a matter of copy-pasting the official list onto our
all_lint_rules.yaml
file.
Step 5: Ignoring warnings on generated files
If you are using code-generation (which you probably should, more onto that later), then consider also ignoring warnings from the generated files.
Whether or not you decide to commit the generated files to your repository, one thing is clear:
Warnings in generated files do not matter to you.
Generated files are out of your control. You shouldn’t edit them, and probably
shouldn’t care about how the generated code looks like either.
As such, instead of polluting your IDE with tons of pointless warning, simply
disabling the linter on generated files is enough.
This can be done by adding some code to your analysis_options.yaml
.
In our case, we will use both json_serializable and Freezed, so the code what we want to add is:
analyzer:
exclude:
# ignore warnings in files from json_serializable, built_value and most generators
- "**/*.g.dart"
# ignore warnings in files generated by Freezed specifically.
- "**/*.freezed.dart"
Step 6: Profit
That’s it!
With these extra steps, we have now enabled some extra sanity checks for our project.
These will come in handy as the project grows.
If you want an example of an empty project applying all of these steps, you can refer to this link.
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
Email