Introduction

As I immersed myself in Flutter, using the build_runner stood out as a real game-changer. It offered a streamlined alternative to crafting extensive code blocks and bypassed the need for reflection, a common requirement in platforms like Xamarin.

Initially, the build times on my MacBook Air M1 were a breeze, and I didn’t give them much thought. But as the project expanded and more code generation libraries joined the mix, things began to slow down. To make matters worse, it seemed like the build_runner kicked in for almost every file change, making the watch option impractical.

It’s surprising how long one can tolerate sluggish build times, but once we hit the four-minute mark, it was clear that action was necessary.

The assessment

The first step in problem-solving is acknowledging the issue. The second is identifying its root cause.

Let’s examine the pubspec.yaml file and identify the packages with runners in use:

Some pertinent questions to ask are:

  • Are all of these packages necessary?
  • Could any be removed?

However, premature optimization aside, there are ways to optimize their performance without removal.

What is build_runner actually doing ?

For our problem, it’s crucial to understand that by default, build_runner recursively loops over all dart files in the project and executes every generator declared in the pubspec.yaml, usually in the dev_dependencies section.

So, essentially, if there are 200 dart files and 7 generators, like those listed above, you can imagine the sheer volume of operations involved.

Run Forrest, run !

It’s apparent that some generators need not run on certain files. For instance, why run mockito on project files or auto_route on data models?

Thus, we need a way to instruct build_runner on which files each runner should execute. The good news is there is a way; the bad news is it’s not immediately obvious how to utilize it.

Enter the build.yaml file.

An example speaks volumes; here’s what we aim to achieve:

targets:
  $default:
    builders:
      freezed:
        enabled: true
        options:
          json: false
        generate_for:
          - lib/core/model/configuration.dart
          - lib/core/model/product.dart
          - lib/core/model/user.dart
          - lib/presentation/cubit/**/*_cubit.dart
          - lib/presentation/cubit/**/*_state.dart
      retrofit_generator|retrofit_generator:
        enabled: true
        generate_for:
          - lib/data/source/**/*api_source.dart
      json_serializable|json_serializable:
        enabled: true
        generate_for:
          - lib/data/source/**/dto/*dto.dart
      mockito|mockBuilder:
        enabled: true
        generate_for:
          - test/mocks/generate_mocks.dart
      hive_generator|hive_generator:
        enabled: true
        generate_for:
          - lib/data/source/persistent_storage/dto/*.dart
      auto_route_generator|auto_route_generator:
        enabled: true
        generate_for:
          - lib/presentation/app_router.dart
          - lib/presentation/page/**/*page.dart
      auto_route_generator|auto_router_generator:
        enabled: true
        generate_for:
          - lib/presentation/app_router.dart
      injectable_generator|injectable_builder:
        enabled: true
        generate_for:
          - lib/injection.dart
          - lib/core/use_case/**/*_use_case.dart
          - lib/data/repository/*_repository.dart
          - lib/data/service/*_service.dart
          - lib/data/source/**/*_source.dart
          - lib/presentation/cubit/**/*_cubit.dart
          - lib/presentation/app_router.dart

As demonstrated, files and patterns can be specified. The more precise the declaration, the faster the build process. It also helps if you have a good file naming convention and clear folder structure.

By centralizing all @GenerateMocks or @GenerateNiceMocks of mockito into one file, I achieved significant speed enhancements. Furthermore, leveraging build.yaml options, like disabling JSON serialization and deserialization code generation in freezed, can further optimize performance.

With these optimizations in place, a full rebuild now completes in under a minute, and the watch option operates smoothly and efficiently.

Conclusion

Creating this file proved to be more challenging than anticipated, especially in finding the correct name for each generator. I had to comb through package documentation and code, engaging in some trial and error.

In my opinion, this is one of the most useful yet least documented aspects of every library.

That’s why I’ve included a slightly modified version of ours here, in case it proves helpful to anyone.

The next steps in maintaining a healthy build time involve reassessing whether certain packages are truly necessary. I can already tell you that freezed is likely on its way out soon. You’ll see why and how in the next article.

Comments