What changed when I rebuilt Span on Android
I built Span on iOS first, then again in Kotlin with Jetpack Compose and ARCore. The second build went faster, as I expected, but several parts could not just be translated.
The maths came across in one go
The measuring maths is plain code with no AR in it, so it was a direct port, along with its tests. All 33 passed first time, including the area ± bug that took a design comparison to find on iOS.
One difference in the port is worth noticing. The measuring session is a struct with mutating functions in Swift. In Kotlin it is an immutable data class, and each change returns a new copy. Both behave like values, and the Kotlin version lets Compose see a change just by comparing the old session with the new one.
Raw ARCore instead of a 3D engine
The usual way to do AR on Android is SceneView, which brings in Google’s Filament 3D engine. But Span’s lines and labels are flat shapes drawn over the camera, not 3D objects. All AR has to do is show the camera picture and say where things are.
So the Android app uses ARCore directly. A small OpenGL renderer draws the camera image. A project function turns a point in the world into a position on the screen, and Compose draws the lines and labels on top. That kept a whole 3D engine out of the app.
Photos as files, not database rows
On iOS, SwiftData has an external storage option that keeps large data like photos outside the database for you. Room has no such option, and a row larger than about 2 MB crashes when Android reads it back from SQLite. So each measurement row stores a file path, and the photo lives in a file. When a row is deleted, the repository deletes its file too.
Drawing the PDF by hand
On iOS, the project PDF reuses SwiftUI views through ImageRenderer. Android’s PdfDocument needs a native Canvas, and Compose has no simple way to draw into one. So a single painter class draws the page with plain Canvas calls, and both the preview and the real file use it. That way the preview cannot drift from what gets shared. It also had small bugs of its own: the logo sat on top of the word “Span”, the footer was too long for one line, and dates came out US style.
Gravity points the other way
The level uses the motion sensors. On iOS, gravity is reported pointing down, so a phone flat on a table reads about -1 on its z axis. Android’s gravity sensor reports the reaction force, pointing up, at about +9.8 in metres per second squared. The bubble maths uses the absolute value, so both platforms read the same, and a test pins one pose from the design to the exact position of the bubble.
Smaller choices
- Only a few screens and a simple back stack, so navigation is a list in a ViewModel and the back button is handled directly, without a navigation library
- Android 12 and newer draw their own splash screen, which ignores the app’s dark window background. It needs its own setting, or the splash is white
- The release build is signed with a key kept out of git. Android only accepts an update signed with the same key, so losing it would mean users have to uninstall to update