Flutter environment with Nix
This guide provides instructions for setting up a Flutter environment with complete support for Linux and Android (including the Android emulator with hardware decoding enabled), on any Linux distribution with the Nix package manager installed. This article also compares Waydroid — an Android simulator leveraging Wayland, complete with hardware decoding capabilities as well — with the official Android emulator. Last, some examples of VSCode launch configurations — including both the emulator and the simulator — are provided to ease development.
1. Environment configuration with Nix
Inside your existing Flutter project, create a flake.nix
in your root folder and add the following configuration to it:
{
description = "Flutter environment";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
android_sdk.accept_license = true;
};
androidEnv = pkgs.androidenv.override { licenseAccepted = true; };
androidComposition = androidEnv.composeAndroidPackages {
cmdLineToolsVersion = "8.0"; # emulator related: newer versions are not only compatible with avdmanager
platformToolsVersion = "34.0.4";
buildToolsVersions = [ "30.0.3" "33.0.2" "34.0.0" ];
platformVersions = [ "28" "31" "32" "33" "34" ];
abiVersions = [ "x86_64" ]; # emulator related: on an ARM machine, replace "x86_64" with
# either "armeabi-v7a" or "arm64-v8a", depending on the architecture of your workstation.
includeNDK = false;
includeSystemImages = true; # emulator related: system images are needed for the emulator.
systemImageTypes = [ "google_apis" "google_apis_playstore" ];
includeEmulator = true; # emulator related: if it should be enabled or not
useGoogleAPIs = true;
extraLicenses = [
"android-googletv-license"
"android-sdk-arm-dbt-license"
"android-sdk-license"
"android-sdk-preview-license"
"google-gdk-license"
"intel-android-extra-license"
"intel-android-sysimage-license"
"mips-android-sysimage-license" ];
};
androidSdk = androidComposition.androidsdk;
in
{
devShell = with pkgs; mkShell rec {
ANDROID_HOME = "${androidSdk}/libexec/android-sdk";
ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk";
JAVA_HOME = jdk11.home;
FLUTTER_ROOT = flutter;
DART_ROOT = "${flutter}/bin/cache/dart-sdk";
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${androidSdk}/libexec/android-sdk/build-tools/33.0.2/aapt2";
QT_QPA_PLATFORM = "wayland;xcb"; # emulator related: try using wayland, otherwise fall back to X.
# NB: due to the emulator's bundled qt version, it currently does not start with QT_QPA_PLATFORM="wayland".
# Maybe one day this will be supported.
buildInputs = [
androidSdk
flutter
qemu_kvm
gradle
jdk11
];
# emulator related: vulkan-loader and libGL shared libs are necessary for hardware decoding
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath [vulkan-loader libGL]}";
# Globally installed packages, which are installed through `dart pub global activate package_name`,
# are located in the `$PUB_CACHE/bin` directory.
shellHook = ''
if [ -z "$PUB_CACHE" ]; then
export PATH="$PATH:$HOME/.pub-cache/bin"
else
export PATH="$PATH:$PUB_CACHE/bin"
fi
'';
};
}
);
}
Change the current directory to you project’s path, and enter the command nix develop
. It will download and set up all dependencies. A flake.lock
file will be generated.
If the project is contained in a git repository, add both flake.nix
and flake.lock
to it.
1.1. No longer necessary specifying SDK in VSCode settings
Since the following two pull requests were merged, it is no longer needed to specify the SDK in your VSCode settings.
Previously (before the release of nixpkgs
flutter
3.16.5) it was needed due to the ecosystem detecting the flutter-unwrapped
SDK instead of the flutter
one.
With these PRs merged, the VSCode extension, the Neovim plugin, patrol, etc. will all detect the correct SDK directory.
The issue that was present in previous versions of Flutter is explained more in detail here.
1.2. Broken VSCode components
NB: Due to the merged PRs mentioned in 1.1, this subsection should not be necessary anymore. Skip it if not affected.
It may be possible that flutter does not integrate correctly with VSCode. In my case, after upgrading from Flutter 3.13.4 to 3.13.8, Start Debugging, Run Without Debugging and all other VSCode tools for running a Flutter app stopped working: the VSCode running status bar would be shown for one or two seconds, then the process would be killed for some unknown reason. Downgrading to 3.13.4 would enable the tools to work again, however, this was only a temporary workaround.
You may first try running flutter clean
.
In my case, flutter clean
was not enough. The solution consisted in renaming/moving /home/manuel/.config/VSCodium
to /home/manuel/.config/VSCodium-backup
and restoring my previously backed up /home/manuel/.config/VSCodium/User/settings.json
and /home/manuel/.config/VSCodium/User/keybinding
. Afterwards, the VSCode tools worked with 3.13.8.
1.3. Malfunctioning Flutter and Dart binaries
NB: Due to the merged PRs mentioned in 1.1, this subsection should not be necessary anymore. Skip it if not affected.
It’s important to consider adding flutter-unwrapped
to your main environment configuration only in case the flutter
and dart
binaries encounter issues. Under specific circumstances, the Nix path to flutter-unwrapped
may be positioned before the Nix path to flutter
in the PATH
, potentially leading to malfunctioning binaries. This is precisely why, in the configuration below, flutter-unwrapped
is defined after flutter
. By doing so, you ensure that the flutter
and dart
binaries provided by flutter
take precedence over those supplied by flutter-unwrapped
.
buildInputs = [
androidSdk
flutter
flutter-unwrapped
qemu_kvm
gradle
jdk11
];
1.3.1. Wrong SDK
In case the wrong SDK (for example 3.13.4 instead of 3.13.8), there should be no need to specify it in your VSCode settings; it
should be enough to run flutter clean
and reopen the project with VSCode. Then 3.13.8 should be used.
1.3.2. Specify another SDK
This section should not be needed unless you want to go ahead and use a different SDK.
Unfortunately, it is currently not possible to load environment variables in VSCode settings, i.e., it is not possible to get the Flutter SDK path this way:
settings.json
:
{
"dart.flutterSdkPath": "${env:FLUTTER_ROOT}",
"dart.sdkPath": "${env:FLUTTER_ROOT}/bin/cache/dart-sdk/bin",
}
Instead, the flutter-wrapped
path needs to be inserted manually, e.g.:
{
"dart.flutterSdkPath": "/nix/store/3fbs65nsjkzk0mjj1085r8z90lkfg9qr-flutter-wrapped-3.13.4-sdk-links",
"dart.sdkPath": "/nix/store/3fbs65nsjkzk0mjj1085r8z90lkfg9qr-flutter-wrapped-3.13.4-sdk-links/bin/cache/dart-sdk/bin",
}
2. Direnv
Direnv is a powerful utility that automates the loading and unloading of environment variables based on the current directory. It’s especially valuable in Nix systems, as it helps manage project-specific dependencies, including Flutter, without the need for global installations.
To make the most of Direnv, follow these steps:
Global Installation: First, ensure you have Direnv installed globally on your system. You can easily install it using your package manager.
VSCode Integration: Install the
direnv
extension for VSCode, which enhances the integration and user experience with Direnv.Project Setup: Create a
.envrc
file in the root directory of your project. This file will contain directives to manage your environment automatically.Example
.envrc
file:use flake
Then add it to git and add the following line to .gitignore:
.direnv
- Automatic Environment Loading: After setting up Direnv, your environment will automatically load whenever you change into the project directory using
cd
or open it in VSCode (NB: the first time you will be asked whether you trust the project). This ensures that project-specific dependencies, like Flutter, are readily available without global installations or manual setup.
By following these steps, the development workflow can be streamlined and dependencies efficiently managed, ensuring a clean and isolated environment for each project.
3. Sqlite support
SQLite libs can be shipped or not at the app level. I recommend shipping them, as this will ship the SQLite interpreter, thus reducing the surface for bugs across different Android (and iOS) versions, but also different Linux distributions and versions. Also, new SQLite features can be safely used on every device.
3.1. Shipping latest sqlite3 libraries (my recommendation)
If the dependency sqlite3_flutter_libs
is specified in your project’s pubspec.yaml
, then
there is nothing else to do. However, sqlite
can still be added to the flake.nix
’s buildInputs
for
debugging purposes..
3.2. Using sqlite3 libs provided by the OS
If sqlite3_flutter_libs
is not specified in the pubspec.yaml
, then the latest sqlite3 version will not be shipped with the app.
In this case, the version of SQLite used will be the one that comes with your OS. See this list for reference.
In this case, sqlite3
’s shared libs need to be added to the flake.nix
by adding sqlite
and sqlite.dev
libs to LD_LIBRARY_PATH, i.e.:
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath [sqlite sqlite.dev vulkan-loader libGL]}";
NB: This means that when the Flutter and Dart versions are updated (through nix flake update
), then these paths
must be manually updated.
4. Problem building Flutter app for Android
When creating a new project, chances are you will encounter the following showstopper when building the Flutter app for Android:
* What went wrong:
Error resolving plugin [id: 'dev.flutter.flutter-plugin-loader', version: '1.0.0']
> A problem occurred configuring project ':gradle'.
> Could not create service of type OutputFilesRepository using ExecutionGradleServices.createOutputFilesRepository().
> Failed to create parent directory '/nix/store/y8c32gi8z15isnjb501806zfygd3fqx1-flutter-wrapped-3.16.7-sdk-links/packages/flutter_tools/gradle/.gradle' when creating directory '/nix/store/y8c32gi8z15isnjb501806zfygd3fqx1-flutter-wrapped-3.16.7-sdk-links/packages/flutter_tools/gradle/.gradle/buildOutputCleanup'
This is due to the generated Gradle scripts that, as of February 2024, are incompatible with the flutter
from nixpkgs
.
4.1. Solution
The solution is made up of three steps.
4.1.1. Change beginning of build.gradle
Change what is in android/app/build.gradle
from:
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
to the code that was generated in previous releases:
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
4.1.2. Keep namespace in build.gradle
Within the same file, make sure the namespace field is not removed, e.g. namespace "com.example.app.project_name"
.
4.1.3. Change settings.gradle
At this point it might already work again. If not, change also android/settings.gradle
from
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
plugins {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
}
include ":app"
to what was generated in the previous releases:
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
5. Android emulator
The official Android emulator needs to be defined in the flake.nix
, as it is already done by the author of this guide. Afterwards, installing and starting an Android virtual device is very simple. The following example shows how to do this.
avdmanager create avd --force --name phone --package 'system-images;android-32;google_apis;x86_64'
emulator -avd phone -skin 720x1280 --gpu host
5.1. Hardware decoding
vulkan-loader
and libGL
shared libs — which are also already present in the flake.nix
— are necessary for enabling hardware decoding.
NB: The --gpu host
part ensures the emulator is running with hardware decoding. If you are using
an Nvidia graphic card or if you are troubleshooting some bugs, then use --gpu guest
to run the
emulator using software decoding. However, this would incur a significant performance penalty.
5.2. Audio issues
If your workstation sound is distorted when running the emulator, a workaround is to run it with the -noaudio
flag.
For example:
emulator -avd phone -skin 720x1280 --gpu host -noaudio
For an actual solution try with this (not tested).
5.3. Wayland support
The official emulator cannot be started natively under Wayland because it is bundled with a specific, older Qt version.
This guide will be updated if a way to use the emulator with the host’s Qt version is found.
5.4. Supported Android version
The emulator supports all Android version, which makes it very practical.
5.5. When to use the official emulator
I usually use the emulator for most development. However, if you mainly use a simulator such as Waydroid, at least consider using the emulator for testing specific things on different Android versions (e.g. across versions 7 to 14).
Edge cases:
- Specific features for specific Android versions (e.g. cloud backups)
- Major changes (Android permissions changing across major versions)
- Database compatibility (in case
sqlite3-common-libs
is not used)
6. Waydroid, an Android simulator
Waydroid is an Android simulator: it is a compatibility layer that allows running Android apps on a Linux distribution. Waydroid primarily leverages KVM (Kernel-based Virtual Machine) and QEMU (Quick Emulator) to create a virtualized environment for Android. It requires Wayland.
6.1. How to install
Follow the Install on Desktops section of the official project.
6.2. Comparison with the official emulator
The official emulator uses a second kernel layer, which constitutes a performance penalty. The emulator also allows multiple configurations, such as running an ARM virtual device on an X86_64 host. In this case there will still be the second kernel layer, but the performance will be very bad due to the host and the guest not sharing the same architecture.
It is possible to use e.g. an x86_64 based Android OS in order to be a lot performant while running on an x86_64 based primary OS (e.g. x86_64 NixOS). The same can be said for ARM and ARM-based macOS machines, or ARM-based Linux distributions. However, there is still going to be a noticeable performance penalty due to the second kernel layer.
With Waydroid, no second kernel layer is needed. Instead, this is what happens: Waydroid runs Android in a container, separated from your existing OS, and uses the same kernel your OS is currently using to bind the hardware to Android for interaction. QEMU provides the necessary hardware abstraction and allows Android to run as if it were on real hardware. It is therefore much faster than the official android emulator. It uses hardware decoding on Intel and AMD GPUs.
However, there are some concerns that make Waydroid adoption questionable: Android versions and Prop setting
6.3. Android versions
Waydroid supports less Android versions than the emulator. As of October 2023, the latest supported version is Android 11.
6.4. Broken prop set
Unfortunately, prop set
does not work on my NixOS machine. For example, none of the following commands worked:
waydroid prop set persist.waydroid.fake_touch "*"
waydroid prop set persist.waydroid.width 720
waydroid prop set persist.waydroid.height 1280
It follows that the user experience is not great, as, for example, swipe-to-scroll does not work due to the fake_touch
prop setting not working, mouse-wheel scrolling must be used instead. This is particularly bad when running a webview within an application, where mouse-wheel scrolling for some reason does not work. Thus, it is not possible to scroll the webview content at all. This observation alone implies Waydroid is not a reliable tool for development yet.
6.5. How to connect to detect Waydroid virtual device
- Figure out the IP address of the Waydroid device under System Settings (in my case it is
192.168.240.112
). - Execute
adb connect 192.168.240.112:5555
in the VSCode terminal.
6.6. When to use the Waydroid simulator
Although I thoroughly enjoy Waydroid for being much more lightweight w.r.t. the emulator, I cannot recommend it due to:
- prop setting not working at all
- lack of new Android versions
- no simple way to rollback to older Android versions, or have different profiles with different Android versions
I personally see Waydroid more of a Android media consumption tool than a development assistant.
Therefore, I advise to entirely rely on the official emulator for development purposes.
7. VSCode launch configurations
This section doesn’t directly pertain to Flutter with Nix. However, given our coverage of the Android emulator and Waydroid, it would be beneficial to include some practical launch configurations as well.
In your development workflow, using VSCode launch configurations is essential. These configurations can be tailored to specific devices or device sets, ensuring a smoother debugging experience.
For both Android virtual and physical devices, it’s recommended to include the --device-user=0
option. This setting ensures that the app is installed for the current user only, rather than globally, as it would be by default. This approach simplifies the uninstallation process.
Replace pixel
with your physical device name.
Feel free to take inspiration from my own .vscode/launch.json
configuration file
(NB: profile and release mode do not make sense with simulators and emulators):
{
"configurations": [
{
"name": "Emulator - debug mode - w/ debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "debug",
"deviceId": "sdk gphone64",
},
{
"name": "Emulator - debug mode - w/out debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "debug",
"deviceId": "sdk gphone64",
"noDebug": true,
},
{
"name": "Waydroid - debug mode - w/ debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "debug",
"deviceId": "WayDroid",
},
{
"name": "Waydroid - debug mode - w/out debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "debug",
"deviceId": "WayDroid",
"noDebug": true,
},
{
"name": "Pixel - debug mode - w/ debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "debug",
"deviceId": "pixel",
},
{
"name": "Pixel - debug mode - w/out debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "debug",
"deviceId": "pixel",
"noDebug": true,
},
{
"name": "Pixel - profile mode",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "profile",
"deviceId": "pixel",
},
{
"name": "Pixel - release mode",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [
"--device-user=0"
],
"flutterMode": "release",
"deviceId": "pixel"
},
{
"name": "Linux - debug mode - w/ debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [],
"flutterMode": "debug",
"deviceId": "linux",
},
{
"name": "Linux - debug mode - w/out debugger",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [],
"flutterMode": "debug",
"deviceId": "linux",
"noDebug": true,
},
{
"name": "Linux - profile mode",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [],
"flutterMode": "profile",
"deviceId": "linux",
},
{
"name": "Linux - release mode",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": [],
"flutterMode": "release",
"deviceId": "linux",
},
],
}