Are we mobile yet?

I never really expected to, but somehow in the last year I started doing mobile app development. I did some React Native on iOS for work, and then a couple unfinished prototypes on and off. I also started an ambitious multiplatform project with Celeste in May last year using Expo, but it was a little rough. We took on a lot of scope at once and started to hit the limits of the existing ecosystem, and were planning to build our own Rust crates for certain things, but overall we weren’t completely convinced by React Native. Anyway, in June, inspired by my recent experiments with Iroh, I had another idea for an app that I wanted. I knew I wanted to use Rust in some capacity (to take advantage of Iroh), so I decided to re-evaluate the current options. My parameters, findings, and decisions are collected here.

Rust core, declarative shell

I like Rust and I like its ecosystem, but I’m not here to sing the praises of Rust. I think it’s a pragmatic choice: it’s reliable, stuff works cross-platform, the ecosystem is decently mature, and I’m familiar and productive with it. C++ is already established for cross-platform mobile app development with the Android NDK and Swift/Objective-C interop, and Rust can take advantage of that. The library ecosystem for Rust on mobile in particular is not super mature, but there is interest. The Android and iOS targets are tier 2, there are tools like cargo-ndk appearing, and there are bindings to platform APIs available.

However, as we’ll see later, the options for doing UI on mobile with Rust are not there yet in my opinion. We’ll take a look at said options, but we’ll also be considering other non-Rust UI options as well. I think this is acceptable. We can write the core logic in Rust for portability, reliability, and performance, but write the UI in something more mature and ergonomic. The non-Rust options are more heavily invested in mobile UI and are practical choices, which is good when you’re trying to actually make something. We’ll also need some way to bridge the UI and core logic.

I’ve worked with React and React-likes a lot in the past and am generally a believer in the declarative UI paradigm. I tried Elm earlier this year to get a feel for the oft-cited Elm Architecture (“TEA”) and enjoyed it too. I’m not well-versed in the details of immediate mode vs retained mode or what have you1, but I know writing declarative UI feels good. Most of the modern mature options offer this, so it’s not going to rule much out, but it ties in to the separation of core and UI nicely. The UI is fluffier, and can be treated as mostly a function of the core’s state. It has some state of its own (the contents of a partially-filled form, or which screen is visible, for example), but most actions are delegated to the core via a narrow, purposeful interface. At the same time, the more serious core logic stays isolated from UI concerns, which leads towards well-defined layers, testability, the functional core/imperative shell pattern, hexagonal architecture, and so on. My current prototype is very reminiscent of TEA, with actions in the UI sending messages to the core which updates a model which then reactively updates the UI.

On rendering

Instead of just listing every framework straight away, there’s a couple specific aspects that I’d like to focus on. The first is rendering. On one end is using the platform2 UI facilities, and on the other end is drawing everything yourself. Using the platform’s defaults gets you something decent faster, and fits in better with the rest of the platform. But since I’m only one person, I don’t really want to learn and maintain separate frameworks for Android and iOS. I also only have one phone, so I can only dogfood one platform at a time. Some frameworks provide an abstraction over the platform views, but this is hard to do well. Abandoning the platform views and doing lower-level rendering makes cross-platform consistency easier, but might not feel platform-native. There are mature options for this though, so it doesn’t mean you have to do the custom rendering either.

Writing separate platform UIs for Android and iOS means using Jetpack Compose or Android XML views and SwiftUI or UIKit respectively. React Native is the most mature option that wraps platform views, and Flutter is the most mature option for cross-platform rendering. Compose Multiplatform also does cross-platform rendering.

Another interesting cross-platform option is to use a webview for rendering, which gets you cross-platform consistency (maybe3). Web UI experience becomes very transferable, and if you’re also doing web for the same app, you can easily reuse code, or even the whole UI. However, you are also now bound to the bad parts of the web platform, and the consensus is that webviews will feel worse, or at least require a lot of investment to feel right. Capacitor and Cordova are established webview-based options. On the Rust side, Tauri mobile and Dioxus mobile both use webviews, but neither are very mature yet.

On maturity

Since we’re being pragmatic here, maturity is a very important factor. I want to focus on making something, not patching holes in the ecosystem or working around bugs and quirks. In particular, Rust is a critical part of the plan, so there also needs to be strong support there.

The official platform UI frameworks are very mature, well-supported, and have large ecosystems. Swift, Objective-C, Kotlin, and Java are all established languages, and I’ve heard good things about Swift and Kotlin. For Rust interop there is UniFFI, which generates bindings in multiple languages for Rust libraries and is developed by Mozilla for use in Firefox. It’s mature, and Swift and Kotlin are among the officially supported languages. I’ve used it a few times now and it definitely beats doing FFI by hand. There are other binding generators that I haven’t tried, but I’m happy with UniFFI and feel comfortable choosing it for bridging the UI and core logic.

Of the cross-platform options, React Native and Flutter are the most mature. React Native uses TypeScript, which some people are quick to criticize, but it’s mature, has a very large ecosystem, and is generally productive. React Native is backed by Facebook and also benefits from all the investment in React. There is also Expo, which is a more batteries-included framework on top of React Native. React Native allows writing C++ (and Rust, by extension) via Native Modules. For Rust bindings, uniffi-bindgen-react-native is young but very promising, and helps build a React Native package for your Rust code using UniFFI definitions.

I haven’t tried Flutter yet, so I have less to say about it. It’s backed by Google and uses Dart. As far as I can tell, Flutter itself is mature, and the Flutter ecosystem is decent, but Dart is not very common and does not have as large of an ecosystem as some of the other options. For Rust interop, there seems to be a few options, including some using UniFFI. I have heard a lot of hate for Flutter. I’m sure it’s productive, but for now I’m inclined to avoid it if I can.

Another cross-platform option is Compose Multiplatform, which builds on Kotlin Multiplatform and Android’s Jetpack Compose. I actually only became aware of it last month when I started this exploration. Previously with Kotlin Multiplatform Mobile you could write your core logic once in Kotlin and reuse it with platform-specific Android and iOS UIs. With CMP, you can write declarative UI in Kotlin similar to writing a Jetpack Compose app, and use that UI on both Android and iOS (and desktop!). It’s not as mature as React Native or Flutter, but it’s backed by Jetbrains and has access to the Kotlin ecosystem. It’s also much easier to write platform-specific code in KMP than in React Native, and supposedly easier to mix with native views than Flutter. For Rust bindings, Gobley handles UniFFI bindings generation and compiling Rust for the different targets.

Rust and the rest

As of now, none of the options for doing UI in Rust are mature enough for me. I do hope we get there someday, but I’m okay with writing UI in something else for now. There are interesting things happening though:

Dioxus targets mobile using a webview, but you write primarily Rust using their rsx! macro, signals, and hooks, similar to the declarative experience of React and JSX. I tried Dioxus mobile in June, but quickly ran into issues, and concluded it wasn’t mature enough yet. The upcoming Dioxus 0.7 version has some very exciting stuff though and I’m definitely keeping an eye on it. Freya is another interesting young project to watch, though it doesn’t target mobile currently. It uses Dioxus’s declarative logic, but drops HTML/CSS and the webview and does its own rendering using Skia instead.

Tauri is sort of in between. You write a web UI using pretty much any web framework of your choosing, and optionally Rust code, and Tauri provides the webview and glues everything together. Being able to take advantage of both the JavaScript and Rust ecosystems, and communicate between both languages easily, is neat. It’s mature enough on desktop and honestly quite pleasant there. Mobile is younger, but I expect it to be a good option in the future if using a webview is acceptable.

Crux looks interesting. I haven’t heard much about it or looked into it much yet, but it seems like it’s a meta-framework that helps you write the core logic in Rust and then separate thin UIs in Swift/Kotlin/TypeScript. There are mentions of Elm, hexagonal architecture, and a purely functional core which is exciting. I need to try it out at some point but I’m watching it as well.

Xilem (introduction here) and the other work of the Linebender organization are also must-follows in the Rust UI space. Most of the projects are more foundational or research-focused, but in the future they might be usable for production or at least have advanced the state of Rust UI. Xilem could support mobile in the future, but that’s a long ways away for now.

Some other Rust UI frameworks that primarily target desktop also have the possibility of being used on mobile. In particular, I’ve seen some very early work towards running egui/eframe and Iced on mobile platforms. For more on Rust for desktop apps, see Are we GUI yet?. Another possible Rust-enabled route is to compile Rust to WebAssembly and use it in a progressive web app. Rust WASM has some challenges still though, and PWAs have their own limitations, so that would not be my first choice.

What else can you make an app with? There are some other cross-platform UI options that I didn’t explore as much. Qt can do mobile cross-platform and QML provides the declarative part, but I’m not very experienced with C++ and it didn’t seem like the best choice given my goals. There is (was?) Xamarin, and .NET MAUI, but without already using C#, I didn’t see any reason to pick them. You could also use a game engine and integrate Rust somehow, if you really wanted to. godot-rust looks cool, but we’re pretty off track now. Let’s make some decisions.

Some decisions

When I started exploring my options last month, I spent some time thinking about framework’s I’d used in the past, and I tried a few frameworks that I hadn’t before, noting their respective strengths and limitations. I knew I didn’t want to write two UI codebases, and I knew I wanted more control over things than React Native or a webview would have readily provided. I was also unenthusiastic about trying Flutter, and had determined that none of the pure-Rust options were mature enough.

Fortunately, I found something I’m happy with. I tried Jetpack Compose for the first time and made a quick proof of concept with Iroh. Then I did another with Compose Multiplatform, and I was pretty impressed. Besides some struggles with Gradle and NixOS, Gobley worked very well, and alleviated my fears about having to figure out building and linking Rust for 7 different targets. It was also my first time using Kotlin, which I have since described as “quite pleasant” and “having many nice things”. I’m enjoying using Android Studio and IDEA too, they’ve had IDE niceties that I’m not typically used to. Writing UI in Compose is declarative and feels good, perhaps even better than writing React, despite its unfamiliarity. It’s been nice to be free from the web platform for once, and writing platform-specific implementations for the things CMP doesn’t provide has been smooth, especially with Rust readily available as well. The only caveat is that I don’t currently have access to a Mac to develop for iOS, so I can’t say anything about that yet. Hopefully works well if/when the time comes to port things. The worst case scenario is that I have write a separate SwiftUI app, with or without KMP, wrapping the same core Rust logic, which doesn’t sound too bad.

After a few weeks with Compose Multiplatform, I haven’t had any major complaints yet. I’ve been working on that idea from last month, and have been able to focus on the app’s functionality and a thin UI layer rather than solving random annoying problems. Rust has also been enjoyable to work with (of course). I’m getting close to having something demo-able, so that will hopefully be up on my microblogging pages soonish.

Rust is already shipping on mobile in some places, but I predict in the future we’ll see more and more mobile apps use Rust in a similar role to C++ today. I’m confident in the Rust core + cross-platform UI approach though, and optimistic about Compose Multiplatform. The foundations are strong, and the ecosystems will continue to grow and mature. There are a lot of tradeoffs and variables to consider though, including your own experience with different languages and frameworks, goals, requirements, priorities, paradigm preferences, and so on. As always, I’m curious to know what you think, via any of the means listed on my homepage.

Thanks for reading!

Bonus: write one mobile app, get a TUI free

Also, I can’t not mention this. My current prototype actually uses Compose Multiplatform for both mobile and desktop, which share the same core logic crate but with different UIs. It can be a bit of a hassle to run both at the same time, and it takes some time to build the Rust crates for multiple platforms all the time. So I had the idea to wrap the core logic crate in a TUI using ratatui. It has access to the exact same actions and the same data to display, but is faster to build and test with. I can use the TUI as the server for the mobile app, or as the client for the desktop app, or even have two instances of the TUI talk to each other so I can iterate on the core logic faster without completely building out the UI for it. This was made possible (and very easy) by the core/UI split. I like it a lot.

Footnotes

  1. For some reading on reactive UI, though, check out Raph Levien’s blog: 1 2 3.

  2. See also: Raph Levien on “native UI”

  3. I’m not super familiar with the options on mobile, but if you’re using different webviews on different platforms (eg. the system webview), you’re not necessarily getting the same look and feel everywhere. It’s similar to developing for multiple browsers then. On desktop, Electron bundles the entirety of Chromium to do cross-platform rendering. Tauri uses the system webview instead, which is lighter, but at the cost of again having to care about multiple webview implementations.