Building an Android app using Nix

How we got here

Note: this page walks through my process of getting to a decent setup for Android development. If you're not interested in why I think this approach is decent and just want to see the code, you might want to skip the first few sections.

So far in my career I had somehow avoided doing any native mobile development. Unfortunately, my sister gave me a good (easy to build, useful, and profitable) app idea, so I decided to try my hand at making an Android app.

I started with the recommended Android Studio and Kotlin, but quickly became overwhelmed by the complex UI, stupendous amounts of XML, and incomprehensible error messages. It became clear that I wouldn't be able to write the app I wanted over the afternoon I had reserved, so I decided to step back and reevaluate my approach.

React Native to the rescue

I realized that React Native could solve some of my problems. Rather than needing to learn a new development paradigm, environment, and language in order to succeed with Android Studio/Kotlin, I could write some "simple" Javascript. While React Native was new to me, I had worked with declarative UI before and have long been a proponent of it.

Unfortunately, when I looked at how to get started with React Native, I remembered one of the reasons I've never played with it before: installing it requires a handful of dependencies, including Node, Android Studio, and Java. I'm not a fan of manually managing lots of dependencies, so I immediately thought about using Nix.

Nix

Nix is a tool that allows you to manage packages and configuration in a declarative and reproducible fashion via a functional language. In practical terms1, using Nix means programs you install both have all of their dependencies specified and are somewhat isolated from your main system, allowing you to install packages without fear of breaking things and without needing to manage their dependencies.

When I need to install packages, especially if they have multiple dependencies, I immediately look to Nix. However, this project was slightly different than the other things I'd used Nix for. I'd historically used Nix to manage programs that I either wanted installed permanently or just wanted to run once or twice. When building an app, I instead wanted to have a complicated development environment setup and in use for a while, but stored in such a way that it both wouldn't permanently bloat my machine's standard environment and could be shared with other developers who wanted to work on the project2.

Luckily, there's a tool that lets us manage individual projects with Nix!

Devenv

I'd seen an interesting project built on top of Nix called Devenv a few weeks prior and thought it might fit. It was made to solve almost exactly the problem I had — configuring a per-project development environment in a way that can easily be shared with other collaborators.

Unfortunately, installing Devenv in the way I wanted to was a bit more complicated than I expected3. There were a few installation options, but none that looked like a clean fit with my system's configuration, which is managed via home-manager using flakes. After a bit of poking around, I came up with the following4:

# Using the standard skeleton for home-manager flakes
inputs = {
  # Include the devenv flake
  devenv.url = "github:cachix/devenv/latest";
  ...
}

# We'll consume devenv in outputs, and insert it into nixpkgs
outputs = {
... home-manager.lib.homeManagerConfiguration {
  pkgs = import nixpkgs {
    system = "x86_64-linux";
    overlays = [
      # Devenv isn't a "real" overlay, but we can fake it
      (self: super: { devenv = devenv.packages.x86_64-linux.devenv; })
    ];
  }
...
}

Now, my home-manager configuration could treat devenv like any other package and ensure it's properly setup on my machines.

Putting it together

Finally, the backstory is complete and we have all the pieces and can put together a reproducible dev environment that'll support React Native for Android!

We'll create a new folder for our project, use devenv init to setup some default configuration files. We'll be touching two of these files, starting with devenv.yaml:

inputs:
  nixpkgs: # Default - allow us to install packages from Nix's repository
    url: github:NixOS/nixpkgs/nixpkgs-unstable
  android-nixpkgs:  # Use a repository that makes Android configuration easy
    url: github:tadfisher/android-nixpkgs/stable

Next, we'll define the dependencies that are needed for our project in devenv.nix:

{ inputs, pkgs, ... }:

let
  # Configure which Android tools we'll need (mostly the recommended ones)
  sdk = (import inputs.android-nixpkgs { }).sdk (sdkPkgs:
    with sdkPkgs; [
      build-tools-30-0-3
      build-tools-33-0-0
      cmdline-tools-latest
      emulator
      patcher-v4
      platform-tools
      platforms-android-33
      system-images-android-32-google-apis-x86-64
    ]);
in {
  # Install various packages from Nix that we'll need for this project
  packages = with pkgs; [
    nodePackages.react-native-cli
    watchman
  ];

  # Ensure our path has various Android SDK things in it
  enterShell = ''
    export PATH="${sdk}/bin:$PATH"
    ${(builtins.readFile "${sdk}/nix-support/setup-hook")}
  '';
}

We've now defined our entire development environment in code, and we we can start using it by running devenv shell! Note that we didn't explicitly install some of the dependencies we knew we'd need — for example, Nix knows react-native-cli relies on Node, so it'll be automatically installed for us.

However, there are various commands that we'll need to remember to run while working on the project. Let's put them into code too by adding to the bottom of devenv.nix:

  # Create the initial AVD that's needed by the emulator
  scripts.create-avd.exec = "avdmanager create avd --force --name phone --package 'system-images;android-32;google_apis;x86_64'";

  # These processes will all run whenever we run `devenv run`
  processes.emulator.exec = "emulator -avd phone -skin 720x1280";
  processes.react-native.exec = "npx react-native start";

This will let us run create-avd to create a standard Android Virtual Device (AVD) for our emulator to use. After that, if we run devenv up, the Android emulator and React Native will both start, leaving us to focus entirely on developing our app.

We now have an entirely reproducible development environment that's stored in code and backed by Nix. If other developers join the project or we come back to it after a long period of time, we know it'll continue to work exactly the same as it does today.

Footnotes

[1] Nix has many things that aren't practical or easily explained. It has a super exciting set of features and I'm betting that it continues to improve and grow in popularity, but in the meantime it's hard to describe in a convincing manner.
[2] There are few things worse than checking out a repo and not being able to build or run it without reverse engineering the previous development environment.
[3] This is overly picky, and there's a one-line command that most people would probably use to get devenv installed. That being said, avoiding that one-liner was a good learning experience, and I'm happier with the end result.
[4] I don't expect many readers to understand this and it's not terribly relevant, but I included it in case someone else comes looking for a "cleaner" way to install devenv with home-manager.

Other articles