03 — Mobile Development

iOS, Android, cross-platform, and the backends behind them

Mobile apps that shipped to real users — and stayed shipped — for more than eleven years.

Native iOS and Android in their own languages, React Native and Flutter where they fit, and the backends, caches and clouds that make a phone more than a thin shell. The eleven-year track is what makes the rest credible.

The foundation

I have spent more than eleven years building and shipping mobile applications to real users — and that single fact is the one that makes every other claim on this page worth reading.

Anyone can list frameworks. The credential that matters is longevity: apps that real people installed, kept, and used across iOS, Android and Windows, over more than a decade. The eleven-year track is not a line on a résumé — it is the evidence that the decisions below were made under the pressure of software that had to keep working after launch.

I work native first. On iOS that means Swift and Objective-C with SwiftUI, Core Data, Core Animation and SiriKit; on Android it means Kotlin and Java against the Android SDK across many OS versions. That native fluency is exactly what makes the cross-platform work — React Native and Flutter at an advanced level, Xamarin at a working level — a deliberate choice rather than a way to avoid a platform.

And a mobile app is never just the client. Behind it sit backends in Node, Spring Boot, Django, Flask, Rails and Go, exposing REST and GraphQL, backed by SQLite on the device and Redis on the server, deployed to Firebase, AWS and Google Cloud. The page that follows is that whole surface, told the way I actually build it.

0+

years shipping mobile apps to real users, across iOS, Android and Windows

0

native platforms maintained in their own languages — Swift/Objective-C and Kotlin/Java

0

cross-platform stacks I run at an advanced level — React Native and Flutter

0

backend frameworks I pair with mobile clients, from Node to Go

Platform matrix

Two native platforms, two cross-platform stacks, one Windows surface.

The first decision on any mobile project is what to build on, and it is rarely the same answer twice. The matrix below is how I reason about it: native iOS and Android each in their own language, two cross-platform stacks I run at an advanced level, and a Windows surface that keeps the shared logic honest. The diagram maps the languages and frameworks onto the platforms rather than pretending one tool covers everything.

LANGUAGE TARGET Swift / ObjC Kotlin / Java React Native Flutter iOS Android Windows solid = native fluency · thin = cross-platform reach

Languages and frameworks, mapped to platforms

A matrix, not a monolith — each platform in the language it rewards.

iOS gets Swift and Objective-C; Android gets Kotlin and Java; Windows gets its own client. React Native and Flutter span the stores when the UI is not fighting the platform, and the column on the right shows where each tool actually lands.

Reading the matrix is the work: it is how I decide whether a feature is native, cross-platform, or a thin client over a shared backend, before a line of UI is written.

  • iOS — Swift, Objective-C, SwiftUI, Core Data
  • Android — Kotlin, Java, Android SDK
  • Cross-platform — React Native, Flutter (advanced)
  • Windows — native desktop client
iOSAndroidReact NativeFlutterWindows
Per platform

What I actually write on each platform.

The platforms are not interchangeable, and the tooling reflects it. Each tab is a platform I have shipped on — the languages, the frameworks, and the judgment about when each one earns its place.

Native iOS in Swift, with Objective-C where it still lives

On iOS I write in Swift and read Objective-C, because real codebases that have shipped for years are rarely pure Swift. I build interfaces in SwiftUI for new screens and keep UIKit where an existing flow depends on it, rather than rewriting working code for the sake of a framework.

Persistence is Core Data when the app owns a real object graph, and motion is Core Animation when a transition needs to feel native rather than approximated. SiriKit handles voice intents where they earn their place — not on every screen, only where a spoken shortcut is genuinely faster than tapping.

  • Swift first, Objective-C read and maintained
  • SwiftUI for new screens, UIKit kept where it works
  • Core Data for the object graph, Core Animation for motion
  • SiriKit intents where voice is actually faster
iOS, in depth

Swift the way it reads on a real screen.

A code panel is more honest than a feature list — this is the shape of the iOS work, not a description of it.

On iOS I lean on SwiftUI for new surfaces and Core Data for the object graph behind them. The snippet below is the kind of thing I write daily: a small, typed model, a fetch the view can observe, and a layout that stays declarative. It is deliberately ordinary — the point of native fluency is that the routine code is clean, not clever.

ContactList.swiftSwift · SwiftUI · Core Data
import SwiftUI
import CoreData

struct ContactList: View {
    @FetchRequest(
        sortDescriptors: [SortDescriptor(\.name)]
    ) private var contacts: FetchedResults<Contact>

    var body: some View {
        List(contacts) { contact in
            VStack(alignment: .leading) {
                Text(contact.name)
                    .font(.headline)
                Text(contact.phone)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
        }
        .animation(.easeInOut, value: contacts.count)
    }
}
SwiftUI · UIKit Core Anim. SiriKit Core Data Swift · Objective-C

The iOS framework set

SwiftUI on top, Core Data underneath, SiriKit where voice is faster.

The frameworks compose: SwiftUI renders, Core Data persists, Core Animation handles the motion SwiftUI does not, and SiriKit exposes the one or two intents where a spoken shortcut genuinely beats tapping. Objective-C stays in the picture because long-lived apps still contain it, and reading it is part of maintaining them.

None of this is added for its own sake. Each framework is in the app because a specific problem — persistence, motion, voice — asked for it.

  • SwiftUI for new screens, UIKit kept where it works
  • Core Data for the object graph
  • Core Animation for native-feeling motion
  • SiriKit intents only where voice is faster
SwiftSwiftUICore DataSiriKit
Android, in depth

Kotlin against the Android SDK, fragmentation and all.

Android rewards explicitness. The snippet below is a Kotlin ViewModel exposing UI state as a flow — lifecycle-aware, survives configuration changes, and does its loading off the main thread. Having shipped across many OS versions, I write Android code that assumes process death and tight background limits rather than hoping to avoid them.

ContactViewModel.ktKotlin · Android SDK
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

class ContactViewModel(
    private val repo: ContactRepository
) : ViewModel() {
    private val _state =
        MutableStateFlow<UiState>(UiState.Loading)
    val state: StateFlow<UiState> = _state.asStateFlow()

    init {
        viewModelScope.launch {
            runCatching { repo.loadContacts() }
                .onSuccess { _state.value = UiState.Ready(it) }
                .onFailure { _state.value = UiState.Error }
        }
    }
}
OS VERSIONS SUPPORTED min peak latest

Many OS versions, real devices

Support the phones users carry, not the one on the keynote.

Android fragmentation is not a problem to complain about — it is the environment. The work is choosing an honest minimum SDK, testing the spread of versions and screen sizes that the actual user base runs, and handling the background and permission rules that tighten with every release.

Java stays in the toolkit alongside Kotlin for the same reason Objective-C does on iOS: shipped code lives a long time, and maintaining it means reading it fluently.

  • Kotlin and Java against the Android SDK
  • Honest minimum-SDK choices
  • Tested across the versions users run
  • Explicit background and permission handling
KotlinJavaAndroid SDKLifecycle
App architecture

The shape of an app, drawn from UI to data store.

A mobile app is a stack of clear layers — and the discipline is keeping them clear so each one can be tested and replaced on its own.

Whatever the platform, the architecture I reach for is the same in spirit: a UI layer that only renders state, a domain layer that holds the logic, a data layer that talks to the network and the device store, and a sync boundary in between. The diagram shows the layers and the single direction the dependencies point.

Keeping the arrows pointing one way is what makes the app maintainable a year later — the UI does not reach into the network, and the network does not know about the UI. The same shape holds whether the client is Swift, Kotlin, React Native or Flutter.

UI LAYER SwiftUI · Compose · RN · Flutter DOMAIN LAYER logic · rules · state DATA LAYER repositories · ORMs SQLite on-device NETWORK REST · GraphQL

UI → domain → data → store

Four layers, dependencies pointing one way.

The UI observes state and emits intents; the domain layer decides; the data layer fetches and persists; SQLite holds the on-device truth and the network carries the rest. Each boundary is a place I can put a test or swap an implementation without the layers above noticing.

It is not architecture for its own sake. It is the structure that let eleven years of apps keep accepting features without collapsing under them.

  • UI renders state, never reaches the network
  • Domain layer holds the logic and the rules
  • Data layer owns network and device persistence
  • SQLite as the on-device source of truth
ArchitectureLayeringTestable
Backends

The server side of every mobile app.

A mobile client is only as good as the backend it talks to — so I build that backend too, in whatever framework fits the team.

Behind the apps I have shipped Node/Express, Spring Boot, Django, Flask, Ruby on Rails and Go with Gin and Gorm. The framework is chosen for the team and the latency budget, not by habit: Node where the team is JavaScript-fluent, Spring where it is a JVM shop, Django or Flask for Python-heavy backends, Rails for convention-driven CRUD, and Go where the per-request budget is tight.

Each one exposes a contract — REST or GraphQL — that the mobile client is built against, persists through ORMs like Sequelize and Mongoose, and caches the hot path in Redis. The process below is the same regardless of which framework sits in the middle.

From contract to a synced device

  1. 01 Define the contract REST or GraphQL schema agreed first — the client and server are built against one contract, not against each other.
  2. 02 Choose the framework Node/Express, Spring Boot, Django/Flask, Rails or Go (Gin/Gorm) — picked to fit the team and the latency budget.
  3. 03 Model the data ORMs — Sequelize or Mongoose — over the relational or document store the domain actually needs.
  4. 04 Cache the hot path Redis in front of the expensive reads, with explicit invalidation rather than hopeful TTLs.
  5. 05 Sync the device SQLite on the device, a sync protocol over the contract, conflict resolution defined before it is needed.
  6. 06 Ship the pipeline Signed builds, automated tests, store delivery — the CI that turns a commit into a build a user can install.
handler.goGo · Gin · Gorm
func GetContact(c *gin.Context) {
    id := c.Param("id")
    if cached, err := rdb.Get(ctx, id).Result();
        err == nil {
        c.Data(200, "application/json",
            []byte(cached))
        return
    }
    var contact Contact
    if err := db.First(&contact, id).Error;
        err != nil {
        c.JSON(404, gin.H{"error": "not found"})
        return
    }
    c.JSON(200, contact)
}

A Go handler, the way it ships

REST and GraphQL, served fast and typed.

The snippet is a Gin handler reading through Gorm with a Redis cache in front of it — the small, repeatable shape of an endpoint a mobile client can rely on: a typed response, a predictable error, and a cache that takes load off the database on the hot path.

Whether the framework is Go, Node or Spring, the contract the phone sees is the same. That is the point of agreeing it first.

  • Go with Gin and Gorm on the tight-budget paths
  • Redis caching the expensive reads
  • Typed responses, predictable error shapes
  • Same contract regardless of framework
GoGinGormRedisREST

The full mobile stack — client to cloud

iOS languages
Swift · Objective-C
iOS frameworks
SwiftUI · UIKit · Core Data · Core Animation · SiriKit
Android languages
Kotlin · Java
Android target
Android SDK, many OS versions
Cross-platform
React Native · Flutter (advanced) · Xamarin (working)
Desktop
Windows clients
Backends
Node/Express · Spring Boot · Django/Flask · Rails · Go (Gin/Gorm)
APIs
REST · GraphQL
On-device / data
SQLite · Redis · Sequelize · Mongoose
Mobile cloud
Firebase · AWS · Google Cloud
Frontend craft
HTML5 · CSS3 · SASS/SCSS · JS ES6+ · TypeScript
Offline-first

When the network fails — and on a phone, it will.

A phone moves through tunnels, elevators and dead zones. An app that assumes connectivity is an app that hangs on a spinner in front of a real user.

I build mobile apps offline-first: the device writes to SQLite immediately and queues the change for sync, so the user never waits on the network to see their own action take effect. When connectivity returns, the queue drains over the REST or GraphQL contract, and conflicts are resolved by a policy decided per entity before it was ever needed.

The diagram traces that loop — local write, queue, sync, reconcile — and the failure mode is the part that matters: the app degrades to read-only rather than silently losing a write. That is the difference between an app users trust and one they abandon.

DEVICE SQLite write QUEUE pending SERVER reconcile STORE Redis · ORM offline sync (REST/GraphQL) resolved state back

Local write → queue → sync → reconcile

The device is the source of truth until the server agrees.

Every write lands in SQLite first and is mirrored into a sync queue. A background sync drains the queue against the server, the server reconciles using Redis-backed reads and the ORM-mapped store, and the resolved state flows back to the device. The user sees instant local feedback and eventual consistency, not a spinner.

Designing the conflict policy up front — last-write-wins, merge, or manual — is what keeps the loop honest when two devices edit the same record.

  • Local write to SQLite before any network call
  • Change queued, drained when connectivity returns
  • Conflict policy defined per entity in advance
  • Degrade to read-only, never lose a write
Offline-firstSQLiteSyncConflict resolution

Offline-first — the contract with the network

On-device store
SQLite — the source of truth while offline
Write path
Local write first, queued for sync
Sync transport
REST or GraphQL over the agreed contract
Conflict policy
Defined per entity before it is needed
Server cache
Redis in front of the expensive reads
Server store
Relational or document, via Sequelize or Mongoose
Failure mode
Degrade to read-only, never silently lose a write

The user should never wait on the network to see their own action. Write locally, sync in the background, and never lose a write — that is the whole offline-first discipline.

Mobile cloud

Firebase, AWS and Google Cloud — chosen by fit.

The cloud is not one decision. A small team needs a backend yesterday; a scaling product needs control over its compute. Each tab is a platform I deploy mobile backends to, and the judgment about when each one is the right answer.

Firebase where a small team needs a backend yesterday

Firebase earns its place when a mobile product needs authentication, a synced datastore and push without standing up infrastructure first. I use Auth for sign-in, Firestore for the structured, queryable data, and the Realtime Database where the access pattern is genuinely a live tree rather than documents.

The discipline is knowing where Firebase stops being the right answer — security rules that have to encode real authorization, and read costs that grow with a naive data model. I design the documents around the queries, not the other way around.

  • Auth for sign-in, Firestore for structured data
  • Realtime Database where a live tree fits the access pattern
  • Security rules treated as real authorization
  • Documents designed around the queries
MOBILE FIREBASE Auth · Firestore AWS Lambda · EC2 · S3 GCP App Eng · Compute one client, the backend that fits the workload

Where the backend actually runs

Managed where it saves work, controlled where it has to be.

Firebase Auth and Firestore stand up a synced backend without infrastructure; Lambda and EC2 give serverless and long-lived compute on AWS; App Engine and Compute Engine do the equivalent on Google Cloud; S3 holds the media a phone downloads. The diagram maps the client onto those options.

The mobile constraint runs through all of it: cold starts are felt on a phone, and uploads have to survive a cellular link. The cloud is chosen so the latency the user sees stays inside budget.

  • Firebase — Auth, Firestore, Realtime Database
  • AWS — Lambda, EC2, S3
  • Google Cloud — App Engine, Compute Engine
  • Chosen by operational fit, latency in budget
FirebaseAWSGoogle CloudServerless
Cross-platform, in depth

React Native and Flutter — a choice, not a default.

Cross-platform stacks let one team ship both stores from one codebase, and I run React Native and Flutter at an advanced level. But the reason I trust them is that I have shipped native first: I know exactly what the bridge is saving me and where it gets in the way. When a screen needs Core Animation, a sensor the bridge does not expose cleanly, or a platform behavior the abstraction papers over, I drop to a native module without hesitation.

Xamarin sits at a working level in the toolkit, which is the right answer in .NET-leaning organizations where the rest of the stack is already C#. The decision among them is never about loyalty — it is about matching the stack to the team and the product, with native always available underneath.

SHARED CODEBASE React Native · Flutter iOS RENDER ANDROID RENDER Swift module Kotlin module

One codebase, two stores, native escape hatch

Shared business logic up top, native modules where the bridge stops.

In a cross-platform app the business logic and most of the UI live in one shared codebase — React Native or Flutter — that renders to both iOS and Android. The parts the abstraction cannot serve drop through to native modules in Swift and Kotlin, so the app never trades capability for portability.

That escape hatch is the whole reason native fluency matters even on a cross-platform project. The shared layer covers the common ground; the native modules cover the edges.

  • Shared logic and UI in one codebase
  • Renders to both iOS and Android
  • Native modules in Swift / Kotlin at the edges
  • Capability never traded for portability
React NativeFlutterNative modules
CI for mobile

The pipeline that turns a commit into an install.

Mobile CI is harder than web CI — there are two toolchains, code-signing, and two store review processes between the commit and the user.

A mobile release is not a deploy; it is a build, a signature, and a submission. The pipeline I run lints and type-checks first, then builds the iOS archive with Xcode and signs it, builds the signed Android APK or AAB with Gradle, and ships versioned artifacts to TestFlight and the internal track before the store. The rule is simple: no green pipeline, no release.

The diagram below shows that fan-out — one commit, two toolchains, two stores — because the cost of getting code-signing or a version bump wrong is a rejected submission and a day lost, not a quick re-deploy.

COMMIT release CHECKS lint · type · test XCODE sign · archive GRADLE signed AAB App Store Play Store

Commit → checks → two builds → two stores

One commit fans out into two signed builds.

The pipeline runs the static checks once, then splits: an Xcode path that builds, signs and archives for iOS, and a Gradle path that assembles and signs for Android. Each produces a versioned artifact that goes to its store's test track before release.

Treating the signed build as the only thing that ships — never a local archive from someone's machine — is what keeps releases reproducible across eleven years of apps.

  • Lint, type-check and unit tests as the gate
  • Xcode signs and archives for iOS
  • Gradle assembles signed APK/AAB for Android
  • Versioned artifacts to test tracks, then store
CICode-signingTestFlightGradle

Mobile CI — stage by stage

Trigger
Commit to a release branch
Static checks
Lint, type-check (TypeScript), unit tests
iOS build
Xcode build, code-signing, archive
Android build
Gradle assemble, signed APK/AAB
Artifact
Versioned build per platform
Delivery
TestFlight / internal track, then store
Gate
No green pipeline, no release
Frontend craft

The web surfaces that sit beside the app.

A mobile product is rarely only the app. There are onboarding pages, marketing surfaces, embedded web views and shared logic that all want the same care as the native code. I build those in clean HTML5 and CSS3, organize the styles with SASS/SCSS so the design system stays a system, and write the logic in modern JavaScript — ES6+ and TypeScript wherever the project tolerates a type system.

TypeScript in particular pays for itself on mobile: a typed contract catches the mismatch between client and server before a user ever sees it, and the same types can flow from the GraphQL schema down into the React Native client.

01

HTML5 & CSS3

The web surfaces that sit beside a mobile app — onboarding pages, marketing, embedded web views — built in clean HTML5 and CSS3 rather than dumped into a framework by reflex.

02

SASS/SCSS

Stylesheets organized with SASS/SCSS so the design system stays a system: variables, partials and mixins instead of copied declarations.

03

JavaScript ES6+

Modern JavaScript for the shared logic and the web layer — modules, async/await, and the language features that make a codebase readable years later.

04

TypeScript

TypeScript wherever the project tolerates it, because a typed contract catches the mismatch between client and server before a user ever sees it.

05

REST APIs

REST for the straightforward resource-shaped endpoints, with versioning and predictable error shapes the mobile client can rely on.

06

GraphQL APIs

GraphQL where a mobile screen needs exactly its data in one round trip — the network round trip a phone pays for is the one worth saving.

The arc

Eleven years, one platform decision at a time.

Foundation, native, cross-platform, backend, cloud — the same engineer, following the work as products grew past their first thousand users.

Read in sequence, the mobile work is one continuous track rather than a list of frameworks. It starts with eleven years of shipping to real users, deepens into native iOS and Android, extends across React Native and Flutter where they fit, reaches back into the backends that make a client more than a shell, and arrives at the clouds that scale it.

What carries through all of it is the same refusal that runs through the rest of my work: the app and the system behind it are one problem, not two handed between two people.

  1. Foundation Eleven years of shipping to real users The track that anchors everything else: more than eleven years building and shipping mobile apps that real people installed and used, across iOS, Android and Windows.
  2. Native iOS and Android in their own languages Swift and Objective-C on iOS with SwiftUI, Core Data, Core Animation and SiriKit; Kotlin and Java on Android across many OS versions. The native fluency that makes the cross-platform choices informed.
  3. Cross-platform React Native and Flutter at an advanced level One team shipping two stores where it fits, dropping to native modules where it does not — plus Xamarin at a working level for .NET-leaning teams.
  4. Backend The server side of every app Node/Express, Spring Boot, Django/Flask, Rails and Go (Gin/Gorm), exposing REST and GraphQL, backed by SQLite, Redis and ORMs — the backends that make a mobile client more than a thin shell.
  5. Cloud Firebase, AWS and Google Cloud Managed datastores, serverless compute and storage chosen by operational fit — the infrastructure that scales an app past its first thousand users.
How I work

The principles underneath the platforms.

The platforms and frameworks change with the product; the principles do not. These are the rules I apply whether the app is native Swift, native Kotlin, React Native or Flutter — the part that turned eleven years of mobile work into apps that kept working rather than a portfolio of launches.

01

Native fluency earns the abstraction

Because I have shipped Swift and Kotlin directly, choosing React Native or Flutter is a measured trade, not a way to avoid learning a platform. I know what the bridge is saving me and what it is hiding.

02

The contract comes first

Client and server are built against one agreed REST or GraphQL contract, ideally typed end to end. The integration is designed before either side is written, not negotiated after both break.

03

Assume the network will fail

A phone moves through dead zones. The app writes locally to SQLite first, queues for sync, and degrades to read-only rather than losing a write or hanging on a spinner.

04

Respect the platform's rules

Background limits, process death, store review, code-signing — these are not obstacles to route around. Working with them is what keeps an app installed instead of uninstalled.

05

Test the fragmentation

Many Android OS versions and a span of iOS devices means testing what users actually carry, not asserting that the latest release covers everyone.

06

Eleven years is the credential

Every claim here is backed by apps that shipped and stayed shipped. The longevity is the evidence — not a framework on a slide, but software that survived contact with real users.

Eleven years of apps that shipped and stayed shipped is the credential. Everything else on this page is true because that one thing is.

Open to the right work

If your product is a mobile app and the backend, the cloud and the offline behavior that make it trustworthy, that is the whole problem I want.

If you are holding a problem that doesn't fit inside one field, that is the conversation I want.

NextCloud & DevOps