Have you ever thought about how much time is needed to run all the Continuous Integration pipelines of your mobile project, assuring your product’s quality?
Imagine that on each new PR (or PR update), a project’s unit tests verification is automatically run. We can think that the time that this pipeline takes to finish has an impact at the end of the month scaled according to the team’s size.
For instance: if a pipeline takes 5 minutes to complete, the team has 10 developers and the pipeline runs at least 2x a day per dev, we’ll have at least 100 minutes of running pipelines per day — what boils down to an average of 2200 minutes (around 36.6h) per month. 😲
How many cups of coffee would it be possible to drink in 36.6h?
Suffice to say that CI jobs’ time can be something critical. If we can save 1 minute from this job, we already benefit much, being the swiftness of the tasks’ flow or the total cost.
Ok, but… How does this apply to Flutter?
The secret here is to reuse artifacts of previous compilations. If we don’t do this, we will always waste that time compiling the same things over and over.
But we must be careful about what we’re storing on cache — we don’t want to skip verifications on modifications that might end up taking bugs to an application.
Example of a pipeline running before and after cache was implemented
Understanding the build process
There are some necessary steps before starting to run the project’s verifications:
- Download Flutter
- Download the project’s dependencies
- Run Flutter build_runner
Each one of these steps can be optimized independently, in case there are no alterations. For example, you might have added a dependency to the project and modified part of the code without having Flutter version modified — that will trigger Flutter version cache and save some CI preparation time, but other compilations continue to be executed because they will generate new cache keys.
How to configure CI cache
We’ll make this using Github Actions, and the cache action to store and retrieve previous compilations.
This action has some necessary parameters for it to work:
path: This parameter means where we’re listing what will be stored on cache. Its value doesn’t limit to one single file or directory, so we might add many directories, for instance;
key: a key in which we’ll store that files’ group;
restore-keys: a list of keys used to retrieve stored cache. For the sake of this example, we will use a single value.
Flutter version cache
Here we store the current Flutter version, to avoid downloading Flutter image every time the CI runs.All we have to save is the bin directory inside the Flutter installation's directory.We will name the key according to the project’s current Flutter version’s title
This cache refers to the dependencies listed on pubspec.yaml.Its key changes according to a hash from the pubspec.lock file, therefore, if there are modifications in the dependencies file, it will discard the previous cache.
At last, we store a cache of the code that was generated by Flutter’s build_runner, and for being a slow process, it is very important that it’s optimized.
In this cache's key, we will add a merge of the asset_graph.json file’s hash with the aforementioned pubspec.lock’s hash.
It’s important to note that if you don’t use build_runner on your project, this step is not necessary.
And where all of this is located on the pipeline?
The cache definitions have to be placed right before the commands that you intend to run in order to make the necessary verifications.
And we’re done! This might be enough for the automatizations of your Flutter project to save up a good amount of your team’s time, for it to focus on what it really needs.