Skip to content
Product & Software Engineering

Mobile App Development

iOS and Android apps that feel native — whichever path gets you there.

Mobile is where many users live. We build apps that feel at home on the platform they run on — fast, responsive and capable of using the device's camera, location, biometrics and notification system without friction. Most clients ship cross-platform first with React Native or Flutter, then invest in a native module for a specific feature that demands it. We know where that line is.

We handle the full lifecycle: UX and interaction design, the app itself, the backend APIs it calls, App Store and Play Store submission, and the over-the-air update strategy that lets you ship a bug fix without waiting for review. You own the developer accounts, the signing certificates and the source code.

what's included

What this covers

React Native and Flutter cross-platform development targeting iOS and Android from one codebase

Swift (iOS) and Kotlin (Android) native builds where platform performance demands it

Offline-first architecture, local storage and background sync

Push notifications, deep linking and in-app purchase flows

Device API integrations: camera, biometrics, GPS, Bluetooth

App Store and Google Play submission, review guidance and metadata

Over-the-air (OTA) update strategy for fast, review-free patches

what you get

Deliverables

  • Submitted apps on both App Store and Google Play under your developer accounts
  • Source code in your repository with build and signing documentation
  • Backend API powering the app, deployed on infrastructure you control
  • OTA update pipeline and crash-reporting integration in place at launch
tools & stack

What we build with

React NativeFlutterExpoSwiftKotlinFirebaseSupabaseFastlaneCodePush / EAS UpdateSentryRevenueCatGoogle Maps SDK
how we think

Principles that govern our mobile engineering

Mobile development surfaces trade-offs that web development can defer indefinitely: battery consumption, memory limits, offline behaviour, OS-level permission flows, and two entirely separate distribution gatekeepers. These principles reflect what we have learned makes the difference between an app users keep and one they delete.

Offline-first, sync later

A mobile app that requires a live connection to display anything is a degraded desktop web experience on a smaller screen. Offline-first architecture means the app reads from a local store (SQLite, MMKV, WatermelonDB) and queues writes for synchronisation when connectivity resumes. The sync layer must be idempotent — a write replayed after reconnection produces the same result as a write processed immediately. This approach also makes the app faster in good connectivity because local reads are always faster than network reads.

OTA updates for the JS layer; native releases for everything else

In React Native and Expo, Expo Updates (EAS Update) and Microsoft CodePush deliver JavaScript-layer changes without App Store or Play Store review — typically under an hour from merge to user device. This is the right path for bug fixes, copy changes, and UI tweaks. Native module changes, new permissions, or any modification to the binary itself require a full store release. We design the app boundary between JS and native deliberately so that the majority of iteration lives in the faster path.

Native modules only when the JS layer genuinely cannot

Writing a native module doubles the maintenance surface: two codebases, two build pipelines, two sets of platform-specific bugs. We reach for native Swift or Kotlin only when a capability is unavailable in the React Native bridge or Flutter plugin ecosystem, or when a specific performance requirement — high-frame-rate animation, ARKit, Bluetooth LE with precise timing — cannot be met in the cross-platform layer. Every native module decision is documented with the specific requirement that drove it.

Permission requests with clear prior context

iOS and Android both apply a 'permanent deny' state after a user declines a permission prompt. A camera permission requested at the wrong moment — before the user understands why the app needs it — is frequently denied permanently. We request permissions at the moment of first use, after showing an explicit in-app rationale screen that explains what the permission enables. We also handle the denial gracefully: the feature degrades cleanly rather than crashing or showing an unhelpful error.

Platform idioms over cross-platform homogeneity

A navigation pattern that is correct on Android (bottom drawer, back-stack behaviour, material transitions) looks and feels wrong on iOS (tab bar, swipe-back gesture, modal presentation). Cross-platform frameworks expose platform-specific components and behaviours; ignoring them produces an app that feels foreign on both platforms. We use platform-adaptive components and write platform-specific variants for interaction patterns where the two idioms genuinely diverge.

decision guides

How we'd choose

There's rarely one right answer — these are the trade-offs we weigh before recommending an approach.

Native vs cross-platform vs PWA — mobile delivery strategy

This decision is driven by required device capabilities, development team composition, acceptable maintenance cost, and how tightly the user experience must conform to platform idioms. Most products in 2024 start cross-platform and add a native module only when a specific capability demands it.

CriterionReact Native / FlutterSwift + Kotlin (pure native)Progressive Web App (PWA)
CodebaseSingle codebase for iOS and Android; platform-specific files where neededSeparate iOS (Swift/Xcode) and Android (Kotlin/Android Studio) codebases maintained in parallelSingle web codebase; runs in the browser with a home-screen shortcut on mobile
Native device API accessFull access via bridge or Dart FFI; most capabilities covered by community packagesFull, direct access to all platform APIs with no abstraction layer or bridge overheadLimited — camera, geolocation, push notifications available; Bluetooth, NFC, ARKit/ARCore not available
UI fidelity and performanceReact Native renders native components; Flutter renders via its own GPU-accelerated engine. Both produce near-native UX for most use casesNative rendering with full access to UIKit/SwiftUI and Material/Compose for pixel-perfect platform fidelityRendered by the browser engine; limited access to system fonts, system gestures, and haptic feedback
App Store distributionSubmitted to both App Store and Play Store as a native binary; standard review process appliesSubmitted to both stores as a native binary; full access to platform review incentivesNo store submission required; distributed via URL; no review cycle
OTA update capabilityJS-layer changes deployable via EAS Update or CodePush without store reviewEvery change requires a store release; no OTA path for production binariesInstant — the web app updates on the next load; service worker controls cache invalidation
Best fitMost consumer and B2B apps; existing React or Flutter team; budget for one codebase, not twoHardware-intensive apps (AR, Bluetooth peripherals, custom camera pipelines), platform showcase apps, or where App Store feature access is a competitive requirementContent-heavy experiences, internal tools, or when rapid iteration and zero install friction outweigh device-API access
what we avoid

Mobile anti-patterns we actively prevent

Treating the mobile backend as an afterthought

Mobile clients are constrained by OS-enforced background execution limits, flaky connections, and strict battery policies. A backend designed for a web browser — long-lived WebSocket connections, large JSON payloads without pagination, no retry semantics — fails mobile clients in the conditions that matter most. The app either drains the battery or becomes unreliable when connectivity varies.

Design the API for mobile constraints from the start: paginated list endpoints, incremental sync endpoints that return only changed records since a given cursor, push notifications via APNs/FCM rather than polling, and payload sizes that work on a 3G connection. Treat the mobile network as unreliable by default.

Ignoring memory pressure on list views

React Native FlatList and Flutter ListView both virtualise their children, but incorrect usage — renderItem functions that create new function references on every render, missing keyExtractor, or heavy per-item layout calculations outside of useMemo — collapses the virtualisation benefit and causes frame drops on mid-range Android devices. This is invisible on a development machine and obvious on a three-year-old Redmi.

Test list performance on a throttled mid-range Android device at every significant list change. Memoize renderItem with React.memo, stabilise callback references with useCallback, and move any layout calculation that does not depend on render state outside the component. Use Perf Monitor in development to verify that JS thread and UI thread frame rates stay above 58fps during scrolling.

Deep linking as a launch-day addition

Deep links — URLs that open the app at a specific screen — underpin push notification navigation, email CTAs, social sharing, and re-engagement campaigns. Retrofitting deep linking to an app whose navigation state is not URL-addressable requires restructuring the entire navigation stack. Done at launch day, this becomes a blocker for marketing and a weeks-long engineering project.

Design the navigation stack to be URL-addressable from the first sprint. In React Native, React Navigation's linking configuration maps URL schemes to screens declaratively. In Flutter, GoRouter provides the same. Register both the custom URL scheme and an HTTPS universal link (Apple) / app link (Android) so links open in the app even when it is not installed.

good to know

Common questions

Should we go cross-platform or build separate native apps?

For most products, React Native or Flutter delivers a native-quality experience at roughly half the cost of two separate codebases. We recommend a native module only when a specific feature — advanced camera processing, ARKit, complex animations — genuinely needs it. We will tell you honestly which camp you are in.

Can you maintain and update the app after launch?

Yes. We offer post-launch support and feature development on a retainer or project basis. OTA updates for JavaScript-layer changes ship without App Store review; larger releases follow the standard submission cycle.

Have something in mind?

Tell us what you're building or stuck on. The first consultation is free — no obligation, no hard sell.