Flutter FAQ: codegen and bundle size

Sailesh DahalSailesh Dahal
6 min read

After 2 years of not writing and ditching my readers, I am trying to get back to writing again. The past couple of years have been really busy with freelancing on Upwork, where I delivered multiple apps for clients that are generating steady MRR.

I’m starting with some topics that might sound trivial but can be confusing to many Flutter developers. These are the kind of questions that often get overlooked but are important to address for smoother development.

Let's try to answer these questions that you may have come across while using flutter and it's build_runner.

  1. Is codegen secretly bloating your app size? TLDR; It's not!

  2. Will static metaprogramming make tools like build_runner obsolete?

  3. How are you handling generated files? Do you commit them to VCS?

  4. Is build_runner getting slower as the project grows? Can you fix it? Yes? How?


What determines the flutter app size, and how to make it smaller?

Flutter provides tools to analyze the size breakdown of your app’s final build, helping you optimize the app size and make it smaller.

There is a special flag that many might not know about, which allows you to view the final contents of the release build. With this command, the Dart compiler will monitor package usage and code utilization within the app, generating a JSON report that details the build contents.

flutter build ipa --analyze-size

The --analyze-size flag lists all the components of the bundle, including the frameworks used, their sizes, and the total asset size of the final bundle. This is how it will look like.

This command also generates a JSON file that we can use to view a TreeMap with dart DevTools.

dart devtools --appSizeBase=ios-code-size-analysis_02.json

Once you run the command, you can see the TreeMap with all the details interactively. You can also click on one node and explore further.

TreeMap view of the size breakdown

Now it's clear that using codegen won't significantly increase the app size (it might only add a few kilobytes), so we don't need to worry about it bloating the app.


What occurs if I list multiple packages in my pubspec.yaml file but don't actually use them?

When you have specified packages/plugins to use in your flutter project, the Dart compiler will treeshake all the unused dart code optimizing the app's size. The tricky part is with the plugin (ones containing native dependencies).

If your pubspec.yaml contains unused plugins, the Dart compiler will treeshake unused Dart code, but native dependencies will remain, increasing the app size.

To prevent this, remove unused packages from pubspec.yaml and regularly check for unused dependencies.

You can use available tools to scan for your unused dependencies and assets from your codebase.

  1. dependency_validator reports any missing, under-promoted, over-promoted, and unused dependencies. Any package that either provides an executable or a builder that will be auto-applied via the dart build system will be considered used even if it isn't imported.

  2. Flutter: Find Unused Dart Files & Assets is a VS code extension which allows you to find unused assets, dart files & dependencies in your project.


If I list an asset in my pubspec.yaml but don't use it within the app, will it still get bundled into the application?

Yes, If you list an asset in your pubspec.yaml but don't use it within your Flutter app, it will still be bundled into the application. This applies to most assets like images, sounds, or general files. The asset bundling process in Flutter does not remove unused assets based on their use in the code. That's why all the assets listed in pubspec.yaml are bundled into the app, regardless of their use, ultimately bloating your app.

What about that message about fonts being treeshaken while I build for android?

The message about fonts being "treeshaken" refers to Flutter's font tree-shaking feature. This feature is different from how assets like images or other files are handled.

Flutter fonts being treeshaken

When you build your Flutter app in release mode, Flutter includes only the font glyphs (characters) that are actually used in your app.

Flutter analyzes your app's text usage and optimizes font files by including only the required characters. This is why you see the "fonts being treeshaken" message during the build.

Will static metaprogramming make tools like build_runner obsolete?

The Dart macro system has introduced support for static meta-programming to the Dart language.

This means that, when the feature is live, we could use annotations and the dart compiler will JIT compile the annotations to code on the fly. Most of the notable packages are planning to adapt this approach to mitigate the short coming of the build_runner (time consuming).

Here's Remi Rousselet talking about rewriting Freezed with macros.

How are you handling generated files? Do you commit them to VCS?

Using tools like build_runner generates a significant number of files, which can be cumbersome to manage, potentially leading to issues like merge conflicts if not handled correctly.

Committing these files to a version control system would be a matter of preference and is somewhat opinionated.

Personally, I do not check these files into VCS when working on collaborative projects to avoid merge conflicts.

If you choose not to check these files into your VCS, you may need to add an additional build step in your CI process to generate these files, which might increase your CI build time.

Is build_runner getting slower as the project grows? Can you fix it? Yes? How?

As your project expands with more files and additional build_runner generators, the overhead increases, leading to longer times for code generation.

This can be optimized by creating a build.yaml file and explicitly telling the generator which files to check for code generation. This can drastically decrease the codegen time.

Here's an example of such build.yaml file.

targets:
  $default:
    builders:
      freezed:freezed:
        generate_for:
          exclude:
            - test/**.dart
          include:
            - lib/**/*data/**/*.dart
            - lib/**/*models/**/*.dart
            - lib/**/*domain/**/*.dart
            - lib/core/error/failures/**/*_failure.dart
            - lib/features/**/*bloc/*_bloc.dart

      injectable_generator:injectable_builder:
        generate_for:
          exclude:
            - test/**.dart
          include:
            - lib/**/*_{bloc,data_source,interceptor,http_client,repository,service,plugin,viewmodel}.dart
            - lib/**/*usecases/**/*.dart
            - lib/core/di/**/*.dart
            - lib/domain/**/*_{factory,api,filters,impl}.dart
            - lib/network/**/*.dart

      json_serializable:json_serializable:
        generate_for:
          exclude:
            - test/**.dart
          include:
            - lib/**/*{data,models,domain}/**/*.dart
        options:
          create_factory: true
          create_to_json: true
          field_rename: snake
          explicit_to_json: true

      retrofit_generator:
        generate_for:
          exclude:
            - test/**.dart
          include:
            - lib/**/*{data_source,http_client}.dart

      stacked_generator:
        stackedBottomsheetGenerator:
          generate_for:
            exclude:
              - test/**.dart
            include:
              - lib/**/*_bottom_sheet.dart

        stackedDialogGenerator:
          enabled: false

        stackedLocatorGenerator:
          enabled: false

        stackedFormGenerator:
          enabled: true
          generate_for:
            exclude:
              - test/**.dart
            include:
              - lib/**/*_view.dart

        stackedLoggerGenerator:
          enabled: true
          generate_for:
            exclude:
              - test/**.dart
            include:
              - lib/**/*_view.dart
              - lib/core/setup/config/logger.dart
              - lib/core/setup/circles_app.dart

        stackedRouterGenerator:
          enabled: true
          generate_for:
            exclude:
              - test/**.dart
            include:
              - lib/**/*_{view,navigator,screen}.dart
              - lib/core/setup/config/routes.dart

Although this reduces the time it takes to run codegen, using this method without proper code architecture will be a nightmare, causing even more problem in the longer run.


That's it for today. If you have any questions on the topic you want me to write about, please feel free to write in comments. I will try to cover as much topics as I can.

Thank you for joining me on this journey, and happy coding! 💻👨💻

0
Subscribe to my newsletter

Read articles from Sailesh Dahal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sailesh Dahal
Sailesh Dahal

Flutter developer, eager to contribute to team success through hard work, and attention to detail.