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:
- Core dependencies
- 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:
- write Elm code
- compile Elm to JS
- include the compiled JS in an
index.html
file and init the Elm app viaElm.Main.init()
- serve the
html
file and JS assets.
Building a mobile app is almost the same, except for a couple of steps:
- include Capacitor-JS related code (in Javascript) – typically, Elm and Capacitor would communicate via ports,
- and because Capacitor-related code will end up being ES6 or later, use a bundler like Parcel
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:
- it compiles Elm code to JS
- it builds a minified CSS file
- then it lets Parcel bundle the whole project into a separate directory (
web
) - and finally, it lets Capacitor "sync" the project which is basically Capacitor copying over the
web
into the Android/iOS project folder.
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.