Building mobile apps using Elm and Capacitor

Aug 15, 2023

Native mobile app dev is still largely object-oriented driven (Kotlin, Java etc). Languages like Kotlin now have a lot of functional programming paradigms supported out of the box (e.g. Arrow).

About two years ago, I wanted to see if Elm can be used to build a meaningful Android app.

I manage my expenses on a simple Google spreadsheet. When I'm on the move, I'd jot down the expenses as notes and then type them out on the spreadsheet once I had access to my laptop. I wanted a way to add expenses directly to my spreadsheet through my phone. The Google Sheets mobile app does not offer a great UX for this.

I ended up building a (highly-personalized) expense tracking Android app using Elm. I've been using this app ever since and has hardly needed a few updates in all this time.

Recently, I updated the "bootstrap" repo that I use to build Elm-based Android apps. (These can be used to build iOS apps as well). You can grab the source-code / clone the repo from here.

But if you're interested in setting up an Elm-based Android app project yourself, here's my notes:

  1. Core dependencies
  2. Bundling logic – how is the project built?

1. Core dependencies

For this project, I use CapacitorJS. This means the whole application runs inside a web-view.

Things have improved quite a bit in the web-view (and mobile browser engine) space in the recent years so building apps that run on web-views is not really a bad thing now.

For styling, I use TailwindCSS. It's simple, clean and has one of the best styling ecosystems that one can ask for.

Custom JS glue-code has to be written to make your Elm app interact with native things (via CapacitorJS) and you'd ideally write these in ES6 or later. So, the app would need a way to be "transpiled" and/or bundled. To do this, I use Parcel. Not exactly the best solution out there but it's far more than enough for a bootstrap. Eventually, I'd like to swap this out with Vite to see how things work.

2. Bundling logic – How is the project built?

If we were building an Elm app, this is broadly the workflow:

Building a mobile app is almost the same, except for a couple of steps:

The bootstrap's project structure is simple:

public
├── css
│   ├── index.css
│   └── style.css
├── index.html
└── js
    ├── elm.js
    └── index.js

Here's more info:

public
├── css
│   ├── index.css <-- all your custom CSS goes here
│   └── style.css <-- this file gets auto-generated by the `yarn build` command
├── index.html <-- main entry-point for the project. Parcel will use this file to build/bundle the project.
└── js
    ├── elm.js <-- this file is auto-generated by Elm during the `yarn build` process
    └── index.js <-- all your custom Capacitor / other JS can go in this file (and other JS files)

The build step does these things:

3. Caveats and explorations

One of the things I realized while building the personal expense tracking app was that Elm routing doesn't work. So I had to resort to using Browser.element and using a custom Page type as part of the Model.

Elm ports can feel a little tedious to hook up with the Capacitor bridge. However, with some good abstractions, using ports can be more streamlined.

Capacitor is not the only JS-native bridge. There are also other tools like NativeScript and it could be worth exploring how that plays with an Elm project.