Integrating Dart's Build Runner with VSCode

VSCode Tasks and Code Generation after Save

ยท

5 min read

There are cases that you need to generate code in Dart to be relieved from writing a lot of boilerplate codes. Some 3rd party libraries utilize it. freezed requires it to generate code to have compile-time errors for union classes. json_serializable requires it to generate necessary methods for serializing into and deserializing from JSON data type. auto_route generates code for type-safe routing in Flutter.

Some of you might think "Why do not we get these functionalities at runtime?". For example, I can annotate a class just like in Python, I get autocomplete and call it a day. While this is out of the scope of this post, the most basic reason is Dart checks code at compile time and removes the unused ones. This process is called tree-shaking and runtime interpretation of a language puts too much work on runtime, which sacrifices a great deal from the performance on device.

Given the scope of the 3rd party libraries of above and the reason why we need to generate code in Dart, we can safely assume that you will have to use build_runner one day, one way or another.

Launching Build Runner

There are two ways to launch build runner:

  • Build launch

  • Watch launch

Build launch is a one-off process. It simply analyzes the project directory, checks out if we ever need to generate some code, generates it and exits. You can launch a build as below:

flutter pub run build_runner build

Watch launch is just like build but with event loop. It runs forever, hence the name "watch". It triggers a new code generation task when it realizes a file has changed in the project directory. You can launch a watch as below:

flutter pub run build_runner watch

While these are simple expressions, I usually add some args that I find useful.

flutter pub run build_runner build lib/ --delete-conflicting-outputs
# or
flutter pub run build_runner watch lib/ --delete-conflicting-outputs

Here, I add lib/ to tell build runner to only check lib directory. The other directories, especially in Flutter, are cluttered with configurations. They are not usually Dart files and even if so, I found out that they won't need any code generation. Giving lib directory makes it faster because now build runner has a narrow scope of file list to look for.

One strange behavior of build runner is that when it finds an already-generated file and needs to generate it again, it asks the user to confirm, which is an odd behavior. If I have changed the source file, I'd like the target file to be generated accordingly. That's why --delete-conflicting-output flag is needed. It automatically answers "Yes" to replace the generated files.

This is how I use build runner, yet build runner is sometimes a problematic beast. You need to constantly check its output to know if it failed or not. So, launching it on an external terminal or integrated terminal in VSCode requires a lot of mouse actions (which all devs agree it is not desirable). That's why I came up with a solution to integrate it with VSCode seamlessly so that I do not have to worry about the generated code.

๐Ÿ—’ Note

Before you mention, I have already tried Build Runner extension for VSCode. It only provides a way to launch it with either status bar or a keyboard shortcut and also it seems to be buggy on Linux. I've got a better solution.

Writing Task for Build Runner

VSCode has a feature called "Tasks". It lets you launch dev tools (without the debugger injected, so it is naturally faster).

So, create .vscode/tasks.json file in your project root and add these:

{
    "version": "2.0.0",
    "tasks": [
        // build runner's build task
        {
            "label": "build_runner-build", // this label will be useful later
            "type": "flutter",
            "command": "flutter",
            "args": [
                "pub",
                "run",
                "build_runner",
                "build",
                "lib/", // only scan lib dir
                "--delete-conflicting-outputs", // do not ask to delete conflicting files
            ],
            "presentation": {
                "echo": true, // write logs
                "reveal": "silent", // i don't wanna see the terminal window
                "focus": false,
                "panel": "shared",
                "showReuseMessage": true,
                "clear": false // i wanna see logs when I want
            },
            "problemMatcher": [
                "$dart-build_runner"
            ],
            "group": "build",
            "detail": ""
        },
        // build runner's watch task
        {
            "label": "build_runner-watch",
            "type": "flutter",
            "command": "flutter",
            "args": [
                "pub",
                "run",
                "build_runner",
                "watch", // the same stuff but watching this time
                "lib/",
                "--delete-conflicting-outputs",
            ],
            "presentation": {
                "echo": true,
                "reveal": "always", // show output for watch tasks
                "focus": false,
                "panel": "shared",
                "showReuseMessage": true,
                "clear": false
            },
            "problemMatcher": [
                "$dart-build_runner"
            ],
            "group": "build",
            "detail": ""
        },
    ]
}

Now, you can CTRL+SHIFT+P, search "Tasks: Run Task" and select what you need to run.

This is cool and all, yet you need to either run build each time you change something in a generative file or you need to spawn a watch process. Each has their own things.

As I've said, build_runner build is a one-off process. It reads, analyzes, generates and exits. This means there won't be any background process that will hog your computer's resources. This also means build_runner build is going to start from the very beginning each time it gets spawned.

On the other hand, build_runner watch initializes build runner and waits. It is especially good if you are frequently generating codes because you won't initialize it over and over again. The subsequent generations will be faster but, at the same time, it will constantly use resources since it is a never-ending process.

What about we run build_runner build after we save a, say, Dart file? That would be conveinent and also not resource draining solution.

Running a Task After File Save

There is a VSCode extension called Trigger Task on Save. Hence the name, it will run a task after a file is saved.

After installing it, you have to add a couple of lines to .vscode/settings.json file.

{
  // other settings
  "triggerTaskOnSave.tasks": {
    // run the task with label `build_runner-build`
    "build_runner-build": [
      "lib/**/*.dart", // if file is a dart file and under lib directory
    ],
  },
  "triggerTaskOnSave.resultIndicator": "statusBar.background", // change status bar bg color
  "triggerTaskOnSave.failureColour": "#c62828", // success color
  "triggerTaskOnSave.successColour": "#2e7d32", // failure color
  "triggerTaskOnSave.showNotifications": true, // show notification if fails or succeeds
  "workbench.colorCustomizations": {},
}

After this, whenever you save a Dart file, it will launch the task labeled build_runner-build, which will inform you with notifications and change the color of status bar to green or red depending on success.

With this method, you don't even have to touch the build runner again. It will do the work for you.