Cljs
Mobile
react native

Making a mobile app with ClojureScript in 2021

7/18/2021
ยท
2338
ยท
profile photo

Work in progress blog!

The below 'blog' is really just an incoherent ramble from when I was making my app, if you want to read my actual post about this, go here. If you want to play with the app I made, the links are here. Also there's a playlist of various videos here, including a demo of the app.
I plan to do a 'part 2' where I move my app to krell (or perhaps make a new one). I did give it a quick go but found too many issues for my liking (js require doesn't work, can't import js files without making them into node modules, repl needs restarting when requires change). David Nolan has said before that Krell is made specifically for his company (vouch) to make their app, and his aim isn't for people like me to have the best experience making toy apps. Though I will try again in a month or so and see where it's at.
In the meantime I am working on 'site', a Clojure platform built on Crux for making webapps without writing backend code (instead you write an openAPI spec with datalog queries/rules). Me and Malcolm are posting videos of the progress here, though they are just raw streams so nothing polished.

Intro

I started working for JUXT around 4 years ago now, and on my first project we made a mobile app using ClojureScript. At the time, I was told this was a groundbreaking technological achievement, though to be honest, it didn't really feel like it. Doing simple things could sometimes take days due to bugs in the underlying rats nest of tooling, and after 6 months we couldn't even publish any updates because the version of expo we were using was no longer supported, and the new ones used a new version of react native which had too many breaking changes to justify updating everything. ๐Ÿคฎ
We abandoned the project after that and never touched mobile apps again, but after reading about Krell, written by the core maintainer of ClojureScript, I became interested again and decided to give it another go.

State of tooling

There are a few ways to make a mobile app with Clojure. This approach uses Graalvm and Sci, and this one uses ClojureScript to build views for an otherwise native app, but currently React Native is the only realistic option if you want something 'stable'. It still won't be as stable as a native app, and nowhere near as stable as a web app, but that is just something that comes with the territory in the native app world.
React Native compiles JS into native code for either iOS or Android targets, so to use ClojureScript, we just need something to turn Clojure into JS that the React Native tools can understand.
๐Ÿ’ก
You can build RN apps for the web too, but don't go thinking that you can easily copy and paste code between 'standard' React and React Native, because React Native uses native mobile components rather than web components. There's no concept of a div, things like buttons are imported from React Native and generally, everything in your views will look pretty different if you're coming from web apps.
Here are the current options for tooling then:
  • Krell - Very simple, very few external dependencies. Designed to get the CLJS into React Native as 'natively' as possible with no added frills. You will need a Mac and Xcode to test your apps on iOS.
  • Shadow-Cljs - Many CLJS developers will already be using this for web projects, but it can build code for a React Native target too. You can either use it with raw React Native or Expo.
    • Expo - A layer on top of React Native that aims to solve many of the more frustrating parts with cross-platform mobile development. With Expo, you don't need to set up Xcode or Android studio. You can test apps on your actual phone just by scanning a QR code, and with Shadow, you will still get a REPL and hot code reloading! You can push code updates instantly without needing Apple or Google to approve them, and you can even develop and deploy an iOS app to the App Store without owning a Mac. However all of this magic comes at a cost, complexity. The main focus of this article will be evaluating the Shadow + Expo combination to see how many issues I run into.
  • Re-Natal - uses figwheel to compile the CLJS and adds some extra features for a more 'Clojure like' experience with React Native. However, the cost of these abstractions is that maintaining such a project is time-consuming, and as such, this project is now very outdated (in mobile terms at least).

What's the app?

I wanted the app to be simple enough that I could build the logic for it in a day or two, but interesting enough that I wouldn't lose motivation. I've been getting into meditation recently and thought I would make yet another meditation app, though it does have a somewhat unique twist.
To give an example, there is a popular set of meditation/breathing exercises known as 'The Wim Hof Method'. The main exercise being the following:
notion image
Now this can be pretty daunting and difficult to remember for some, and others may find it difficult to stick to without a reminder, so Wim Hof has an app that will play audio clips of each step and keep track of things like how many cycles you have done.
This is also the case for many other activities such as yoga. An app will be made with a series of 'activities' such as 'tree position' with maybe an image showing the position and a voice explaining it. After some duration, the next step will start.
But I think all of these routines can be condensed into a simple data structure like so:
(def power-breath
  {:name "Wim Hof Power Breath"
   :description "Active Deep inhale, passive exhale "
   :activities [{:title "Breath in"
                 :duration 2000}
                {:title "Breath out"
                 :duration 4000}]})

(def exhale-hold-breath
  {:name "Hold your breath"
   :description "Exhale completely and retain as long as possible"
   :activities [{:title "Exhale fully"
                 :duration 2000}
                {:title "Hold your breath for as long as possible"
                 :duration nil}]})

(def inhale-hold-breath
  {:name "Hold your breath"
   :description "Inhale deeply"
   :activities [{:title "Inhale deeply"
                 :duration 3000}
                {:title "Hold your breath"
                 :duration 15000}
                {:title "Exhale"
                 :duration 3000}]})

(def wim-hof-round
  {:name "Wim Hof Method"
   :description "30 cycles of power breaths and breath holding"
   :activities [{:title "Power Breaths"
                 :cycle-count 30
                 :activity power-breath}
                exhale-hold-breath
                inhale-hold-breath
                {:title "Meditation"
                 :duration nil}]})
As you can see, this is a very flexible structure, if there is a duration a timer could show you how much time was left, if no duration exists, it relies on the user to click a button to move on. There could easily be an audio recording or image for each step etc. So all the app needs to do is:
  • Rotate through each activity and keep track of which step is the 'current activity'
  • Render each 'current activity' map (have a render component for duration, title, description etc)
I managed to set up a basic mobile app that does all of this logic in under an hour. You can see the recorded live stream of me doing so below, and the commit is here.
To set up starting point seen in the beginning of this video:
  • You can start from here. I made some initial modifications to disable hot reload in expo web, as well as tidy the indentation to my preference and remove some unwanted code. I also added some setup for clj-kondo which I would recommend to anyone. You can checkout my initial commit here for details.
Next episode I will add a nicer UI, follow me on twitch to get updated when I stream, or check this page in a few days as I will be keeping it updated. Feel free to comment on the youtube videos or here.

Detour into the land of Krell

Before I jumped into making my UI, I had a bit of a research around what I would need to do to get two important features working: HealthKit integration (to save mindfulness or fitness minutes), and background audio (Otherwise the app stops working if the user locks there phone, preferably it would act like the music app and show the 'now playing' screen).
However it seemed that both of these things could only be done if I ejected from expo. Because of this, and also some small but annoying issues with the shadow+expo setup where errors would be invisible, I decided to try and build what I have in Krell instead.

React Native without Expo

To get started with krell, you first have to setup a proper react native environment, I followed this guide. After an hour I still couldn't get Xcode to build my project, apparently many other people are also having issues but even after following all the steps in this thread, nothing was working. Eventually I figured out the issue, my dependencies were installed on an M1 Mac and something in there was not happy with that (see here if you have the same issue). I was later told that such events are 'just part of the mobile dev experience'. For all Expo's limitations, I can definitely see why its so popular, not having to deal with Xcode or Android Studio is a pretty big perk sometimes.
Once you can run the default react native project without expo, I followed the following steps to convert my expo app into a krell app:
  • expo eject - This created a useless index.js file which I needed to delete
  • Run yarn ios to build the project and run it in the simulator
    • This step failed first time I ran it with an unreadable long message. I opened XCode, loaded the `ios/kalmroutines.xcworkspace file and clicked build to see a better error.
    • Turned out to be because react native wants a root level index.js file. Wasn't sure how to achieve this with shadow-cljs, so just decided to switch to krell
    • added krell deps and followed guide here
    • changed init function like so:
      • ;; old
        (defn init
        {:dev/after-load true}
        []
        ...
        
        ;; new
        (defn ^:export -main
          [& args]
        ...
    • Add build.edn file
    • rewrite ns requires that use :default to use $default (see here)
    • finally everything runs, but get a runtime error Unable to resolve module ../src/src/stories/utils.ts. hmm seems like krell resolves requires differently..
    • Try renaming ../src/stories/utils.ts (and others) to various combinations in a classic display of 'if you don't understand, guess'
    • realise I need to do a full repl restart for require updates to be processed (issue)
    • after a while, realise that there must be a bug in krell with using js/require and files in your src path
    • Move my stories folder to the root of the project and require ../stories/foo.ts
    • Yay it works! But now I get a new error: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory ๐Ÿคฆโ€โ™‚๏ธ
    • Guess xcode uses a lot more ram than expo, I only have 8gb โ˜น๏ธ close some stuff to free up space
    • running the repl again crashes with ReferenceError: Can't find variable: cljs
    • decide its probably worth just trying to get a basic krell example working rather than move my whole app over, so follow the guide properly
    • note: everything is much slower with xcode than expo, builds etc take probably 10 times longer
    • On running npx react-native run-ios I get an error about
      • ld: warning: Could not find or use auto-linked library 'swiftCoreGraphics'
        ld: warning: Could not find or use auto-linked library 'swiftUIKit'
        ld: warning: Could not find or use auto-linked library 'swiftDarwin'
        ld: warning: Could not find or use auto-linked library 'swiftFoundation'
        ld: warning: Could not find or use auto-linked library 'swiftMetal'
        ld: warning: Could not find or use auto-linked library 'swiftObjectiveC'
        ld: warning: Could not find or use auto-linked library 'swiftCoreFoundation'
        ld: warning: Could not find or use auto-linked library 'swiftDispatch'
        ld: warning: Could not find or use auto-linked library 'swiftCoreImage'
        ld: warning: Could not find or use auto-linked library 'swiftQuartzCore'
        ld: warning: Could not find or use auto-linked library 'swiftCore'
        ld: warning: Could not find or use auto-linked library 'swiftSwiftOnoneSupport'
        Undefined symbols for architecture arm64:
        
    • However after doing a 'clean' and running in xcode I get no errors and it all starts ๐Ÿคท
    • Great! At this point I have a working krell app that I can play with and everything works. CIDER setup is just cider-jack-in-cljs and choose krell option.
    • I have to run it in the simulator because I don't have a cable for my phone... expo can do it wirelessly, but whatever. Time to try and move my app over
    • I first start by trying to require a single simple .js file

Publishing to the App Stores

One advantage of Expo is that you can update your App without re-submitting to the app stores (there are some exceptions, such as updating the Expo SDK, but they are fairly rare). However, you will need to go through this process at least once, and there are quite a few steps involved. This was my journey in getting my app onto both the android and apple stores.
  • Make a google developer account and pay the $25 ransom โ˜น๏ธ
  • Make an Apple developer account and pay the $100 ransom ๐Ÿ˜–
  • Make sure app.json contains the correct information
  • Run shadow-cljs release app and make sure everything works in the Expo Go app after running expo publish
  • Run expo build:android -t app-bundle and expo build:ios
  • Have a cup of tea, the build process took just over 30 minutes for both ios and android, though its hard to complain since Expo offers this service for free. You can choose to build on your own hardware if you have a Mac for the ios build, but you will need to spend some time setting up the additional tools.
  • For iOS
    • Go to https://appstoreconnect.apple.com and create a New App with your desired information. You should see the bundleIdentifier from your App.json in the dropdown.
    • Download Transporter, drag the .ipa downloaded from Expo into the window and click 'Send to App Store Connect'
    • Have another cup of tea (The upload took 4 minutes for me, but it took an additional 20 minutes for the build to show up on AppStoreConnect)
    • Once the processing is done, you need to jump through 1 more hoop about encryption (My app doesn't use any so I just ticked 'no') and then your app will be published to the TestFlight store, you can add up to 100 users to test things out.
    • When you are ready to go public on the big boy app store, you'll need to make sure you have your assets in order. As this includes making a whole bunch of screenshots for every screen size and orientation available, this is actually quite a painful process, which was made a lot easier with fastlane and appure
      • notion image
    • Once submitted, apple will take some time to review your app and decide if it's worthy, once approved you'll be public on the store!
    • It only took 3 hours for apple to approve the app, which is much better than the week I remember waiting before. Maybe I got lucky thoughโ€ฆ
  • For Android
    • Go to play console and follow the steps to create an app. You will need to make a privacy policy in Policy โ†’ App content. I used https://app.termly.io/ to generate one and hosted in on pastebin
    • Follow the steps in the Closed Testing section to invite users to test your app before you release it publicly

Expo EAS Build

Almost 2 weeks into my journey, I realised that I would need to add some libraries that required 'native code'. Expo includes a whole bunch of native code itself and tries to cover all the common edge cases, but anything that requires a custom token of some sort (e.g healthkit interop, analytics, in app purchases setup etc) will mean expo can no longer run or build your app. However, the Expo people have clearly had a good think about this because they are in the process of building a new system called EAS.
You can find out more here but basically it is a rethink of expo build and has several advantages:
  • The resulting binary only contains the dependencies you have specified in your package.json, rather than everything Expo thinks you might want. This results in a massive reduction in app size for almost every app.
  • You can build and deploy apps to the ios and android app stores even if they contain native code
  • Builds are much quicker, the whole process for both iOS and Android took 45 minutes on the old system but only
  • The eas submit command makes submitting apps to the app stores much easier and less painful
  • You can still push updates without needing to re submit to the app stores for review
There are downsides to this new system however:
  • It isn't free. I enrolled in the one month free trial, but should I choose to continue using the service it will cost me $29 a month. However Expo say this is just because the feature is unreleased, you can only get access to pre release features if you pay (pay to beta test, makes sense ๐Ÿค”). Once EAS build is ready for prime time there will be a free tier.
  • It is still in alpha, I didn't experience any issues but its still worth mentioning
  • The first time running eas build I experienced this error error Unable to resolve module ./App from /build/workingdir/build/index.js: This was because expo had unhelpfully generated a useless index.js file, presumably because it's uncommon to use ClojureScript for Expo projects. To resolve this issue, I had to set shadow to write to the root directory, and then modify the outputted file to fix references to my imported javascript files. This did work, but its a pretty nasty thing to do so I'm not going to give details, instead I decided to just not eject for now and revisit the problem later.
    • Edit: user bpringe has come up with a better solution for this, you can see here for details

Testing on Android

Two weeks into my journey I had a more or less finished app, I could make routines, play routines, get notifications when a new activity started, and I started actually using the app whenever I needed to do something that I would normally use the timer app on my phone or google home. Up until this point I had only ran the app on the iOS simulator and my own iPhone, React Native may advertise itself as 'learn once run anywhere' but I was skeptical. So I gave a few friends access via TestFlight on iOS (a somewhat difficult process...) and via the Play store or sending the APK file directly (a much easier process). This testing immediately highlighted some issues:
  • My iPhone has a notch and no home button, I had added manual padding in some of my views (bad) and as a result things looked wrong on a friends iPhone 8 without the notch. I fixed this by testing in an iPhone 8 simulator and using React Native's 'SafeAreaView'.
  • The app wouldn't open at all on android, this was difficult to track down because the crash report google gives you is useless, but eventually after figuring out how to get an Android emulator running on an Apple Silicon Mac (use an apple silicon build from here, if you get Error running adb: adb: device offline then install R rather than S) I traced the issue to react-native-svg and 'solved' it by downgrading 3 major versions as suggested ๐Ÿคท
  • My android using friends commented that the modal for adding a routine felt 'janky' and 'out of place', it seems this is a common practice on iOS but not on android. However, I didn't really see an easy fix, and because android users are lesser humans decided to just ignore their whingeing.
  • Android on react native complains if you use setTimeout with a duration of more than a few minutes. There is a long running GitHub issue regarding this which was eventually closed with the advice of using a third party library like react-native-background-timer, however as that library requires an expo eject I decided to just ignore the warnings. It would mean that the timers won't work properly when the app is backgrounded, but the push notifications will because they use a separate scheduling system and that was enough for me.
  • Some buttons that used an absolute positioned TouchableOpacity didn't work on Android, following this helped fix it.
Overall though I was quite pleased with how things worked cross-platform, after all there are often cross-browser issues to deal with in web development (looking at you Safari), and considering android and iOS are in a totally different level of incompatibility, I think the issues I faced here are totally acceptable.

Issues I had

  • If you make a mistake in your code, you will receive no warning from the browser, if you refresh the page you will instead get a previous version of the compiled bundle. For this reason you need to have the shadow-cljs terminal visible at all times so you can see any compilation errors.
  • Numerous times I experienced a stale bundle that would not update, despite shadow-cljs saying Build completed. The only solution I found was to restart both the shadow and expo processes. This one was a real productivity killer and was also difficult to reliably reproduce (and therefore probably difficult to fix).
  • Thought I could get away without testing on an Android emulator as things are supposed to work cross platform, but even with a very basic version of my app my Android using friend reported a crash when changing page. I then had to figure out how to get an Android emulator working on an M1 mac (use an apple silicon build from here, if you get Error running adb: adb: device offline then install R rather than S) and fix the issue
  • Weird z index issues with a drop down component, not sure how to debug as no dev tools
  • NativeBase has a few weird bugs, including not switching to dark theme automatically. If I had to start again I wouldn't use it.

0 comments