EngineeringMay 15th, 2024

Building Rise for mobile

Insights from planning, building and shipping a native app for iOS and Android.

Tim von OldenburgRick Pastoor

We just released the first version of Rise for iOS, and we are quite happy with it. Planning, building and finishing a product like that is always fun to read about, so we though we’d write about our experience building it and a few of the technical decision we made along the way.

The whole project was stretched over 6 months, mostly by one engineer (Tim), with some help from Rick on the client and with a bunch of time from Iain on the backend to tackle authentication.

We thought it would be fun to share a bit about our thoughts and insights we gathered before, during and after this first part of the project.

Researching our options

When starting this project, we already had a bunch of discussions about our approach. Actually everyone in the team at some point brought up going full native. If you don’t want any compromises on quality, it’s the way to go. But there’s a big cost to it: reusing any of the things from our client we have been spending so much time on in the last years would be impossible. So Tim started his research phase with this question: ‘Is there a feasible technology available where we can reuse a lot from our existing code which could give us a highly performant multi-day calendar view?’

We decided on planning a cycle (we run on two-week cycles) where Tim would explore and write about the different options and try to set up as many demos as he could. Ultimately, mobile (as most software) is about how it feels when you actually get to use it on a device. Tim looked at a bunch of them, like Flutter, NativeScript and Capacitor, but those didn’t made the cut for various reasons: Flutter would not allow us to reuse code, for example. Ultimately, there were two most promising routes: Capacitor and React Native. Capacitor is most comparable to how we’re running Rise on desktop, where we are using an Electron based shell built by the amazing team at ToDesktop. React Native is well known (for various positive and negative reasons) and allows you to write React which compiles down to native elements.

Tim started prototyping. Here’s a screenshot of his approach:

Tim explored in Notion, mostly in code

Running Capacitor was easy, but Tim quickly noted a bunch of issues with it: our user interface elements would need a lot of adjustments to get right, since they are way smaller than what works well when using your fingers to control the app. But most importantly: tapping, scrolling and panning around the calendar was super buggy. The calendar view in our app is both very complicated and highly optimized, and having to debug that translation to mobile would range anywhere between hard to near impossible — next to the fact that any change there would also directly impact our desktop experience.

React Native turned out to have its own set of challenges: while you can share code for the business logic, you cannot share React views. But ultimately, after working on building a set of demo views, we were all impressed by the performance of it.

Building the app

Get into habit of shipping

We always try to ship things as quickly as possible. The faster you’re getting something out there, the quicker you’re learning and getting feedback. The excitement in the team really helps get, keep and gain momentum. In our web stack this is simple: merge your code and a few minutes later it’s live. Pull request reviews are optional – you just merge whenever you feel comfortable. On web, we heavily use a really simple tagging structure to hide new features.

If you want quick feedback, screenshots work well too

On mobile, this obviously is slightly more difficult, since we’re not loading code live from a server. We have to go through Apple and Google to ship apps. This process is made slightly more complicated by the additional steps in building we have to take to transform our TypeScript into a Native binary, which then has to be signed and uploaded to Apple.

We briefly looked into using EAS, which is a service from the amazing team at Expo, that allows you to build your apps somewhere in the cloud. It’s relatively expensive, so we first tried our own hand at it.

It’s actually not super complicated. We have a small script that does the following:

doppler run -- npx expo prebuild --platform ios --clean
doppler run --config=prd --project=mobile -- xcodebuild -workspace ./ios/Rise.xcworkspace -scheme Rise -configuration Release -destination generic/platform=iOS DEVELOPMENT_TEAM=[TEAM IDENTIFIER] archive

As you can see, we’re using doppler to manage our secrets in a secure way. There are a few commands before this to make sure the environment is set up correctly and all dependencies are set. The only thing left after this is to go to XCode → Archive and upload the artifact to Apple. From there it automatically is distributed to ourselves in TestFlight.

In TestFlight we created a group for Early Risers, and if we want to share the build with that group, we simply tick the box.

It’s a bit annoying that people cannot unsubscribe to updates themselves, so those are messages we have to process on our end, otherwise this is a pretty smooth process and shipping a build does not take more than a few minutes to get done.

One could argue that it would be nice to automate this even more – and we obviously could. We would say: try finding a good balance. Building TypeScript apps is not always without the occasional hiccup, and in this phase we don’t want to waste time fixing a broken build pipeline that is doing way more builds than we actually need. Especially with a team of 7, like us, this is good enough.

How we’re sharing code between the two projects

Rise is set up as a monorepo. We started with one folder that contains the backend (written in mostly Ruby) and a folder that contains the web client. To validate, Tim copied over most of the source of the client to see if he could get it to work. Which it did.

When Dany (our other amazing frontend engineer) joined, he started to organize our features in smaller libraries. We didn’t follow a super strict path there, so it meant that we would have libraries around one feature which contained both tsx files as well as utilities to transform our data into the structures we use in certain features. That does not work for React Native, since one import can then break the build.

Tim started by creating a shared-frontend workspace, and started to move certain parts of the client codebase there. That’s a pretty painful and slow process, so we also decided to speed thing during that part of the project up by directly linking into the client workspace. Doing it in that way allowed us to keep the changes of the original client to an absolute minimum. There is a big risk though: it’s way too easy to break the mobile build by adding a web specific import or adding a tsx file to a library that is used from the app. Often Dany and I would not notice until Tim started to work on the mobile app again, which obviously is super frustrating.

Our workspaces setup

That’s why we dedicated a bit of time somewhere early in the first quarter to eliminate all references to the original client and move all logic and our sync engine to the shared frontend workspace. That was a lot of work, but it also dramatically improved the structure of the web client. The biggest benefit is that it is now super easy to share logic, and TypeScript is guarding that changes in that shared space do not break either client.

One thing we did notice and which could have helped us a lot: to do have plenty of logic scattered all over our UI components. While that really helped us to iterate super fast, at this point it slowed us down because we had to either copy over code or refactor the web client first. Also there we took a pragmatic approach to refactor some parts and annotate others so it’s easy to see where we duplicated code.

Finally, we quite heavily depend on the delegation pattern throughout our codebase to isolate responsibilities. One example is a class we dubbed Platform, which is a layer that allows us to define one interface to commonly needed lower level features, like a way to locally store data. Each client implements a delegate for our Platform, so in our shared code we can simply use our Platform instance to store values either in LocalStorage or MMKV. Keeps things really clean and readable.

The dreaded UI thread and butter smooth dragging

The good news for us: our core turned out to be pretty fast. MobX is very performant for us, and because we do all our operations optimistically, the UI is rarely blocked by network requests. The one part where we did invest most of our time was in making sure our scrolling and dragging is as smooth as possible.

TBD

Authentication

TBD

Debugging :(

TBD

Other insights

Managing dependencies is super tricky. Dependency github action that builds it.

Notifications?

Maybe something on plugins we use/built

Making sure we’re ignoring the ios/android folder

Performance of some parts/Hermes and date objects

Bonus! Rise for Android

While we started with multi-platform in mind, we didn’t plan to launch on Android. As I was preparing out launch post about iOS, I knew we would get that question. So I figured: why don’t I follow the instructions to build our React Native app on Android and see what happens.

It somewhat worked straight up:

Android works! Somewhat

Turns out that after downloading Android Studio and installing Java, I only had to make a few tiny adjustments to get it to build. All in all it took me a few hours to get it to run. Most time was spent on setting up a proper build pipeline which would make it easy to get a signed build which we could start distributing towards the Play Store.

Long story short: we invited the first users into our Android alpha track on the same day as we launched the iOS app in the App Store and we are continuing to roll out improvements and fixes.

After launching

Reception of Rise for Mobile has been universally positive. We intentionally launched with a limited but polished featureset, focusing on shipping a foundation that’s a solid starting point for the next phase of Rise. React Native and Expo and the ability to share so much of our frontend codebase has been working great for us, and – even though we were all quite skeptical about React Native in the beginning – it performs pretty well. Launching cross-platform, what’s the ultimate promise that often falls short, really delivered for us.