Announcing Red Badger’s experimental open-source framework for writing native User Interface (UI) applications for Mobile, Web, and Desktop.
An open-source framework to help you write native User Interface applications for Mobile, Web, Desktop and beyond, faster and with fewer bugs — by sharing your application’s behaviour as a single and easy-to-test Rust core that you can reuse with confidence on every platform.
Most apps need to be built three times – once for iOS (hopefully in Swift), once for Android (hopefully in Kotlin), once for web (hopefully in TypeScript) – with no code reuse, no saving of effort, and no learning from issues. Clearly, there’s no economy in doing the same thing three or more times, and so the industry has been on a years-long hunt for a way of sharing effort and reusing code — a way of building apps just once.
But a good solution to this problem seems to forever elude us. Maybe we’re trying to answer the wrong question.
These are the options we have today. We can use React Native (but probably have to think about the Web separately). We can use Kotlin Multiplatform Mobile (KMM), or Flutter (with Dart). Or we can go hybrid and use something like Capacitor with Ionic.
None of these are bad choices, but they all have significant downsides. We think we can do better than that — mostly by deciding which bits are worth sharing and which bits are not.
Welcome to Crux, our answer to that.
The secret, in our opinion, is to pull the behavior of the application into a shared core — write it once, test it once, and deploy it everywhere. This is the important part of your application — the “crux”, if you like — and it’s the part that needs to be right! That means well-tested and consistent.
Then add some platform-native UI (and side effects) on top, on each platform. These bits are better left to the native platform anyway — nothing will beat Swift UI for building a truly iPhone-native user experience (an experience that may differ quite a bit from its counterpart on Android, for example, and certainly from the experience in a Web browser). Helpfully, these days, all three platforms have good choices for declarative UI frameworks, eroding the edge that React Native used to have. And since the behavior — states, actions, transitions, information to show — is defined in the core, the UI code should be quite straightforward and minimal, making the shell just a very thin layer around the core.
KMM recognises this too — in fact, CRUX and KMM are similar in this regard (they both defer to native UI). One difference is that, in CRUX, the core is stateful, but side-effect free, and therefore easily testable (inspired by the Elm architecture). Importantly, updating the UI is considered a side effect, and all side effects are pushed out to the edge (the platform native shell).
If you only have to write the behavior of your app once, and you can test it easily, then you can have high confidence in the way your app works. If all you did was test the core, you would still be sure that the app works in the way you expect (except for small bugs in the UI, which are usually fairly obvious and easy to fix).
Ports, Adapters and testability
Alistair Cockburn, in his 2005 post “Hexagonal Architecture”, described the Ports and Adapters pattern as “allow[ing] an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.”
This is the essence of CRUX. This is what can make an application reusable across all platforms, and, importantly, very easy to test.
The pure core responds to events from the “driving” side, and sends effects (requests to perform side effects) to the “driven” side.
Having a core that is free of side effects ensures your business logic remains isolated and testable. This is important. Most organizations that write software today either don’t do enough testing (probably because it’s hard to do), or spend too much time and money on testing (also, probably, because it’s hard to do).
Making it easy to do is absolutely key to building software that is a delight to use.
When the core is pure, testing it simply becomes a matter of checking that it responds in the way you expect it to when you send it a specific message. All tests can be unit tests, no complex mocking, stubbing, harness, hooks or, god forbid, frameworks needed. And unit tests are fast, so you can have thousands of them if you need to. With CRUX, you can run a full suite of tests in milli-seconds. This changes the dynamic completely. High confidence from immediate feedback improves productivity by insane amounts.
Rust is rapidly becoming a default choice for building all kinds of systems and applications.
It’s solid and reliable, safe and secure, fast and efficient.
It also has a very modern and advanced type system that makes it suitable for modeling complex business domains (and, in CRUX, these types flow across the boundary into the native shell’s type system, meaning that breaking changes in the core will stop the shell from compiling — this is a great thing).
You don’t need to write as many tests as you would in, say, TypeScript, because you only need to test logic (not the sloppiness of the language itself). Most people using Rust would agree that the statement “if it compiles, it works” is true, more often than not.
We built CRUX in Rust because it’s not only good at all these things, but it also has a great portability story. In order for CRUX to work, we need to support Foreign Function Interface (FFI) calls to pass messages between CRUX and many different native shells. We also need to be able to compile CRUX to a dynamic library (for Android), to a static library (for iOS) and to WebAssembly (for Web). These cross-platform binaries are trivial to produce using the Rust toolchain, and integration with platform-native tooling is straightforward.
Incidentally, compiling the core to WebAssembly (Wasm) actually ensures that it remains pure — side effects are not possible in the vanilla Wasm sandbox.
We also believe that Rust is the best choice for encapsulating and sharing your app’s behavior. This is more controversial. You could argue that Rust is a systems language and wasn’t intended to be a high level application language. But people are using it very successfully here too. In fact, it is becoming more and more popular in more and more places (web, cloud-native, embedded, dev tooling, games, and even space). Interestingly, 21% of all new code going into the Android operating system is Rust and Rust is now in the Linux kernel.
And, famously, Rust has been voted the most-loved language for 7 years on the trot. There’s a reason for that.
Ultimately, Rust shifts bugs to the left, where they are still fresh, and way cheaper to fix. It helps you get it right, from the start. You put some extra effort in now, so you don’t have bigger (and growing) problems later. This helps prevent teams from getting swamped by a long tail of hard-to-fix problems (something typical of large TypeScript projects).
So now I need to know 4 languages?
Swift, Kotlin, maybe TypeScript — and now Rust as well!
Ok, that does sound like a tall order (especially as Rust is famous for having a steep learning curve).
Kotlin, Swift and TypeScript are similar to Rust in many ways, and they are arguably more forgiving, so they shouldn’t be too hard to pick up (and they all now have declarative UI frameworks similar to React). Alternatively, find a platform expert to work on the native part, your app core will define a strong-enough protocol to keep everyone in sync.
If you’re curious about how Rust can actually make building applications easier (and cheaper) in the long run, then we encourage you to explore how CRUX can help you.
Also, please get involved, contribute a pull request, raise an issue, or ask a question in our Zulip channel.
CRUX is built, with love, at Red Badger.