Flutter comes with a rich set of commands to help you build your Flutter project features fast. You use these project commands to test or build your Flutter apps. These commands get complex when you have different build configurations and workflow automation. Often, you end up using a lot of commands, creating a bunch of shell scripts, and more.

GNU Make or Make helps simplify commands you need for your Flutter project. Although Make is not a 100% replacement for your shell scripts, Make compliments those.

Make and the Makefile

Make is a tool that controls the generation of executables for your program. Commonly used for building C++ source files. In the context of Flutter, you use Make to simplify your Flutter commands or scripts.

If you use Windows OS, read this guide on how to install Make on your machine.

To use Make, you need to create a Makefile on the root directory of your project (relative to pubspec.yaml):

touch Makefile

Make has concepts of rules which you can define to tell Make how to run or build your program.

targets:    dependencies...
            commands...

In Make, you can have a set of commands for your Flutter app that might look something like this:

run-prod:
	$(FLUTTER) run --flavor prod --dart-define=is_something_enabled=false

deploy-prod:
	$(FLUTTER) lint
	$(FLUTTER) test
	$(FLUTTER) run --flavor prod

Unlike having a god shell script, Makefile has a light syntax and an expressive way of declaring your tasks and their dependencies.

Make for Running Your Flutter Project

Flutter has an amazing list of helpful commands and allows you to add custom launch args by using --dart-define, a.k.a. Dart Defines.

Flutter runs your project by simply calling flutter run in your root project. It gets complex when you have a couple of build flavors and configurations.

For example, running your app with a custom flavor for development:

flutter run --flavor dev

or your app with a custom flavor and launch arguments:

flutter run --flavor dev --dart-define=use_crashlytics_logging=false --dart-define=use_sentry_logging=false

Whatever your launch commands are, they should be documented so that others know how to interact with your project.

To use Make commands for running your Flutter project, add the following to your Makefile:

.PHONY: run
run:
	flutter run --flavor dev --dart-define=use_crashlytics_logging=false --dart-define=use_sentry_logging=false

.PHONY: build
build:
	flutter run --flavor dev

.PHONY: run-prod
run-prod:
	flutter run --flavor prod --dart-define=use_crashlytics_logging=false --dart-define=use_sentry_logging=false

.PHONY: build-prod
build-prod:
	flutter run --flavor prod

.PHONY: run-components
run-components:
	flutter run --flavor dev --dart-define=show_components_only=true

Then, you can launch your Flutter project like:

make run

Make for Generating Dart Code

In a typical development setup, you almost always have a command for generating the Dart code using build_runner.

Most of the time, you run:

flutter pub run build_runner build --delete-conflicting-outputs

or without the conflict flag:

flutter pub run build_runner build

In some of my projects, I also have apps that use build_runner’s multiple configurations.

In the build.yaml , you can define custom configurations:

targets:
  $default:
    builders:
      build_web_compilers:entrypoint:
        options:
          compiler: dart2js
        dev_options:
          dart2js_args:
          - --no-minify
        release_options:
          dart2js_args:
          - -O3

and you call the build option by passing a --config value, for example, building for release:

flutter pub run build_runner build --config release

To use Make commands for generating Dart code, add the following to your Makefile:

.PHONY: codegen-cached
codegen-cached:
	flutter pub run build_runner build

.PHONY: codegen
codegen:
	flutter pub run build_runner build --delete-conflicting-outputs

.PHONY: codegen-release
codegen-release:
	flutter pub run build_runner build --delete-conflicting-outputs --config release

Then, you can run the build_runner commands like:

make codegen

Make for Flutter CI Automation

If you’re here, you’ve probably already advanced in Flutter and make use of CI/CD platforms like Github Actions, CircleCI, and Bitrise for automation. Using these systems, you interact with CI/CD environments using CLI commands.

Depending on the CI platform are you using, you can create custom Make rules for each.

This is an example config file when you are using Github Actions:

name: Main Workflow

on: push

concurrency:
  group: main-${{ github.ref }}
  cancel-in-progress: true

env:
  flutter_sdk: "3.0.2"
  java_sdk: "12.x"

jobs:
main:
    name: Main Job
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ ubuntu-latest ]
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          ref: ${{ github.ref }}
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6
      - name: Setup Java
        uses: actions/setup-java@v1
        with:
          java-version: ${{ env.java_sdk }}
      - name: Setup Flutter SDK
        uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.flutter }}
      # Example use of GNU Make starts here
      - name: Generate Code
        run: make codegen
      - name: Run Unit Tests
        run: make tests
      - name: Run GUI Tests
        run: make integration-tests
      - name: Build Dev Build
        run: make build
      - name: Deploy Dev Build
        run: make deploy-build
      # Example use of GNU Make ends here

You can create as many rules as you’d like here, but make sure it’s going to be helpful for the rest of the team and the project.

Other use cases

There are other areas in your project where Make can be helpful like for git pre-commits, code lint and formatting, generating code coverage reports, and many more.

Here’s an example of a Makefile you can use for the default Flutter app:

ROOT := $(shell git rev-parse --show-toplevel)
FLUTTER := $(shell which flutter)
FLUTTER_BIN_DIR := $(shell dirname $(FLUTTER))
FLUTTER_DIR := $(FLUTTER_BIN_DIR:/bin=)
DART := $(FLUTTER_BIN_DIR)/cache/dart-sdk/bin/dart

# Flutter
.PHONY: analyze
analyze:
	$(FLUTTER) analyze

.PHONY: format
format:
	$(FLUTTER) format .

.PHONY: test
test:
	$(FLUTTER) test

.PHONY: codegen
codegen:
	$(FLUTTER) pub run build_runner build --delete-conflicting-outputs
    
.PHONY: run
run:
	$(FLUTTER) run

# Git
.PHONY: fetch-main
fetch-main:
	$(shell git fetch origin main)

.PHONY: rebase-main
rebase-main:
	$(shell git pull --rebase origin main)

It’s a wrap!

Make is an alternative way to work with the commands of your Flutter projects. It helps you define and self-document tasks which are helpful for maintaining projects at scale, or when onboarding new engineers.

Other tools like using custom extensions for VS Code and your preferred IDE can also help simplify your life. Although, these extensions and IDE specific aren’t something you can use in your automation, eg. running IDE extensions for launch configurations in CI/CD.

Give it a try today!