Getting started: Creating your Flutter project

The optional not-so-optional steps that Flutter doesn't mention when creating your project.

Rémi Rousselet

9 minute read

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 used implicit-cast and enabled the avoid_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 like no_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 that all_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.

comments powered by Disqus