Mobile-app core in Rust #1: Overview
I recently created the core logic of several mobile apps using Rust. I found this to be an excellent architecture, so I plan to explain its details over several entries.
The first entry will summarize an overview and the concept of this architecture.
Why am I doing this?
If you're a Rust enthusiast, you'll understand: programming in Rust gives a great sense of reassurance. Compared to other programming languages, there are many situations where you can be confident there are no bugs.
Because programming in Rust is so comfortable, the longer you use it, the more you want to write as much code as possible in Rust. I’m no exception, and that’s the initial reason I started trying out this architecture.
What began as mere curiosity turned out to be a much better architecture than I expected. I want more people to try it, which is why I’m starting this blog entry series.
Overview of the Architecture
In this architecture, the core functionality of the app is implemented entirely in Rust, while the UI parts are implemented natively.
For bridging Rust code with Kotlin/Swift, I use a crate called UniFFI.
Below is a diagram quoted from the UniFFI site:
UniFFI diagram
The portion enclosed by the black solid line in the middle of the diagram is automatically generated by UniFFI. This means we only need to write the logic part in Rust (the lower part of the diagram) and the UI part (the upper part).
Let’s look at a sample UI implementation using Swift UI.
import SwiftUI
struct TokenListScreen: View {
@State private var tokens: [Token] = []
var body: some View {
NavigationStack {
List(tokens, id: \.id) { token in
NavigationLink {
TokenDetailScreen(id: token.id)
} label: {
Text(token.account)
}
}
.navigationTitle("Auth2")
.task {
await load()
}
}
}
private func load() async {
do {
tokens = try await Auth2Bridge.shared().listTokens()
} catch {
print("failed to load tokens: \(error)")
}
}
}
Here, Auth2Bridge
is a struct implemented in Rust and exposed as a Swift class by UniFFI. The corresponding Rust code looks like this:
#[derive(uniffi::Object)]
pub struct Auth2Bridge {
inner: Arc<Auth2>,
}
#[uniffi::export]
impl Auth2Bridge {
// snip...
pub async fn list_tokens(&self) -> Result<Vec<Token>, Error> {
let inner = self.inner.clone();
rt().spawn(async move { inner.list_tokens().await }).await?
}
// ...snip
}
This Auth2Bridge
is essentially just a wrapper around Auth2
. I won’t explain the reasoning here, it might be covered in following entries.
The list_tokens
method of Auth2
is as follows. This is the actual implementation of list_tokens
:
pub async fn list_tokens(&self) -> Result<Vec<Token>, Error> {
Ok(self
.db
.list_tokens()
.await?
.into_iter()
.map(Token::from)
.collect())
}
It’s a simple function that fetches tokens from the database and returns them.
If you look back at the Swift code, you’ll notice that the async function in Rust is treated as an async function in Swift as well. By using UniFFI, you can expose Rust functionality to Kotlin/Swift without having to worry much about the language differences.
Additionally, the return value Vec<Token>
is similarly transformed into a Swift version by UniFFI, allowing it to be used naturally in Swift code as shown below:
List(tokens, id: \.id) { token in
NavigationLink {
TokenDetailScreen(id: token.id)
} label: {
Text(token.account)
}
}
Pretty neat, right?
Similarly, UniFFI generates glue code for Kotlin, making it feel as if the Rust code is a native part of Kotlin.
There are many solutions for writing cross-platform mobile apps, but given how convenient this approach is, I think Rust is definitely a viable option.
The Role of Jetpack Compose / Swift UI
Another important factor is the emergence of new UI libraries like Jetpack Compose and Swift UI.
These libraries make creating UIs very simple. Writing UIs natively with these libraries while sharing core functionality implemented in Rust seems like an ideal architecture.
Those with experience in React will find their experience helpful in learning Compose or Swift UI. These libraries have concepts and patterns that feel familiar to React developers, making it easier to pick them up quickly. I had about five years of React Native experience, and I quickly became comfortable creating UIs with these.
Here’s a sample app. It took me about a day to implement the Rust library and Android app, and only 2-3 hours to build the iOS version using the same library.
Sample Application
I’ve published a simple sample app created with this architecture. All the code introduced in this article is from this app.
https://github.com/typester/auth2
Though simple, it includes features useful for real-world apps, such as:
- Integration with a database (SQLite)
- Database migration
- Integration with KeyStore or Keychain
- Output of tracing logs as app logs
I plan to explain these features in more detail in future entries.
How does it compare to React Native?
I have about five years of experience with React Native, but after discovering this architecture, I can’t imagine going back to it.
The biggest reason, as you might expect, is that I want to write in Rust!🤣 But there are also legitimate reasons beyond that:
- Performance
- Ease of implementation
- Ease of adopting the latest features on iOS and Android
Regarding performance, it almost goes without saying. Compose and Swift UI are native frameworks, so they often outperform React Native, which runs on JavaScript.
As for ease of implementation, if you’re developing an app for either Android or iOS alone, the cost of implementation feels nearly identical. That’s how well-designed Compose and Swift UI are. For apps targeting both platforms, you’ll need to implement the UI separately for each, which makes it costlier than React Native. However, since you can share a common library written in Rust, it’s not twice the cost. Personally, I believe the other benefits outweigh this additional effort.
Another advantage of Compose and Swift UI is their ability to adopt the latest features immediately. From the moment a new OS is released, you can leverage its features. With React Native or similar frameworks, you often have to wait for them to support those new features.
Future Entries
Next up will likely be an article about setting up the development environment. I’d love to hear your thoughts or requests for topics via Mastodon. Thank you for reading this far!