Mobile Apps

Mobile App Development


Most businesses don't need a mobile app. A responsive web application works for the majority of use cases and avoids the complexity of app store submissions, platform-specific development, and the ongoing maintenance burden that native apps demand.

But some requirements genuinely call for a native mobile presence: offline functionality in environments with unreliable connectivity, push notifications that need to reach users immediately, camera and sensor access for field work, background processing for tracking and sync, or simply meeting users in the context where they already spend hours each day. When a mobile app is the right answer, cross-platform development delivers iOS and Android from a single codebase without doubling your development investment.


The Decision: Mobile App or Web App

The first technical decision is whether you need a mobile app at all. This is not a marketing question. It is an engineering question about what capabilities your application requires and whether those capabilities are available through the web platform.

The constraint: Mobile apps have significant overhead (app store review, platform fragmentation, update distribution) that web apps avoid. The trade-off only makes sense when you need capabilities the web cannot provide.

Capabilities that require a mobile app

The web platform has expanded significantly, but certain capabilities remain exclusive to native apps or are substantially more reliable when implemented natively.

Offline-first functionality

Web apps can cache data using Service Workers, but the model is fundamentally online-first with offline as a fallback. Native apps can be architected offline-first, with a local database as the source of truth and sync as the networking layer. For field service apps, inventory management in warehouses, or any context where connectivity is unreliable, this distinction matters.

Push notifications

Web Push exists but has significant limitations: no support on iOS Safari until iOS 16.4 (and even then, only for home screen web apps), lower delivery reliability, and users are far less likely to grant permission. Native push notifications are more reliable, more customisable, and users expect apps to send them.

Background processing

Location tracking that continues when the app is not in the foreground, background sync that runs on a schedule, Bluetooth communication with nearby devices. The web has limited background capabilities through Background Sync and Background Fetch APIs, but they are constrained by browser policies and do not match what native apps can achieve.

Hardware integration

Camera access is available on the web, but native apps have finer control over focus, exposure, and raw image data. Bluetooth Low Energy is available through Web Bluetooth, but with limited browser support and restricted pairing flows. NFC is not available on the web. Biometric authentication through Face ID or fingerprint is more deeply integrated in native apps.

When a web app is sufficient

If your application primarily displays data, accepts user input, and communicates with a server, a responsive web application is likely the right choice. Web apps deploy instantly, work on any device with a browser, require no app store approval, and can be updated at any time without user action. The build vs buy decision often starts here: do you need native capabilities, or will web suffice?

Data entry and display: Forms, dashboards, reports, content management.
E-commerce: Product browsing, cart, checkout. Mobile Safari and Chrome handle payments well.
Internal tools: Staff-facing applications where you control the devices and connectivity.
Not a reason: "Our competitors have an app" is not a technical requirement.
Not a reason: "Marketing wants an icon on the home screen." PWAs can be added to the home screen.

Platform Choices: Native, Cross-Platform, or Hybrid

Once you have determined that a mobile app is necessary, the next decision is how to build it. The three approaches differ in development cost, performance characteristics, and platform fidelity.

Approach How it works Trade-offs
Native Separate codebases in Swift (iOS) and Kotlin (Android). Each app is purpose-built for its platform. Double development effort, two skill sets, harder to keep feature parity.
Cross-platform Single codebase (React Native, Flutter) compiles to native components on each platform. Shared code, faster development, occasional platform-specific edge cases.
Hybrid Web app wrapped in a native container (Cordova, Capacitor). UI is rendered in a WebView. Faster to ship if you have a web app, but performance and feel are noticeably worse.

When native development makes sense

Native development is appropriate when you need every last drop of performance (games, video processing, augmented reality), when you need deep platform integration that cross-platform frameworks do not expose, or when your organisation already has dedicated iOS and Android teams with established codebases.

For most business applications, the performance difference between native and cross-platform is imperceptible. The 60fps animations, smooth scrolling, and responsive touch interactions that users expect are achievable with React Native or Flutter. The cases where native is genuinely necessary are narrower than many assume.

Why we use React Native with Expo

For business applications, we use React Native with the Expo framework. This is a deliberate choice based on engineering trade-offs, not trend-following.

React Native: genuinely native UI

React Native renders actual native components, not web views. A <View> becomes a UIView on iOS and an android.view.View on Android. The UI feels native because it is native. The JavaScript bridge handles application logic while the platform handles rendering.

Expo: managed complexity

Expo abstracts the native build toolchain. You write JavaScript and TypeScript. Expo handles Xcode configuration, Gradle builds, native module linking, and the dozens of platform-specific configuration files that native development requires. When you need custom native code, Expo's "bare workflow" or config plugins allow it without abandoning the framework.

Over-the-air updates

JavaScript bundle changes can be pushed to users without app store review. Bug fixes and minor features can ship in minutes rather than days. This is particularly valuable during the weeks after launch when issues surface under real-world usage.

Caveat: React Native is not appropriate for every project. Compute-intensive applications, games, or apps requiring deep platform integration may need native development. We evaluate each project on its requirements, not on a default technology choice.


Offline-First Architecture

Mobile users lose connectivity. They walk into lifts, drive through tunnels, visit sites with poor signal, or simply experience the variability of mobile networks. Applications that assume constant connectivity provide a degraded experience at best and fail entirely at worst.

Offline-first architecture treats the local database as the source of truth. The network is a synchronisation layer, not a prerequisite for functionality. This is a fundamental architectural choice that affects data modelling, state management, and conflict resolution.

The naive approach

Many mobile applications treat offline as an error state. API calls fail, the user sees a "No connection" message, and the application becomes unusable. Some applications cache the last-fetched data for display purposes but disable all write operations when offline.

This approach is simple to implement but brittle in practice. Users cannot complete work when connectivity drops. Partially completed actions may be lost. The application fights against the reality of mobile network conditions rather than accommodating them.

The robust pattern

Offline-first applications queue operations locally and synchronise when connectivity returns. The user can continue working regardless of network state. The application shows clear status indicators but does not block interactions.

User action

Creates, updates, or deletes data

Local write

Persisted to SQLite immediately

Sync queue

Operation added to outbound queue

Background sync

Queue processed when online

Conflict resolution

Server reconciles if needed

Implementation considerations

Offline-first is not free. It requires additional engineering effort in several areas.

Local database schema. The local SQLite database (or equivalent) mirrors the server schema with additions for sync state: timestamps for last modification, flags for pending sync, identifiers for conflict tracking. Schema migrations must work on both client and server.

Sync queue management. Outbound operations queue locally with retry logic. The queue must handle ordering dependencies (create parent before child), idempotency (the same operation may be attempted multiple times), and failure handling (what happens when an operation is rejected by the server). The same principles that govern server-side background jobs apply here, adapted for the mobile context.

Conflict resolution. When the same record is modified on multiple devices while offline, the server must resolve the conflict. Strategies include last-write-wins (simple but potentially loses data), merge (field-level resolution), or manual conflict resolution (surface to the user). The appropriate strategy depends on the data being modified.

UI state management. The user interface must reflect sync state without creating anxiety. Subtle indicators show pending changes. Clear feedback confirms successful sync. Error states explain what happened and what the user should do. The application feels reliable even when operating offline.

When it matters: Field service applications, inventory management in warehouses, inspection apps for sites with poor connectivity, any application where users cannot wait for network requests to complete their work.


Push Notifications

Push notifications are the most abused feature in mobile development. Used well, they provide timely, valuable information that users appreciate. Used poorly, they train users to disable notifications or uninstall the application entirely.

The technical architecture

Push notifications route through platform-specific services: Apple Push Notification service (APNs) for iOS and Firebase Cloud Messaging (FCM) for Android. Your server sends the notification payload to these services, which deliver it to the device. The device may be asleep, the app may not be running, and delivery is not guaranteed.

1

Token registration: The app requests a device token from APNs or FCM and sends it to your server. Tokens can change; the app must re-register on launch and handle token refresh.


2

Server dispatch: Your backend sends notification payloads to APNs/FCM with the device token. This requires authentication certificates (APNs) or API keys (FCM) and handling of the different payload formats.


3

Platform delivery: APNs and FCM route notifications to devices. Delivery depends on device state, network conditions, and platform-specific throttling. Neither service guarantees delivery or delivery timing.


4

Device handling: The OS displays the notification based on user preferences and app state. Tapping the notification launches the app with context about which notification was tapped.

Permission strategy

Both platforms require explicit user permission to send notifications. Once denied, permission is difficult to regain (the user must navigate to system settings). The timing and context of the permission request significantly affects grant rates.

Defer the request: Ask for permission at a contextually relevant moment, not on first launch. After the user enables a feature that benefits from notifications is ideal.
Explain the value: Use a pre-permission screen that explains what notifications will be sent and why they are valuable. Then trigger the system dialog.
Do not: Request permission on first launch with no context. Users reflexively deny permission requests they do not understand.
Do not: Send marketing notifications through push. Users expect transactional, time-sensitive information. Abuse leads to disabling or uninstalling.

Notification types

Transactional notifications inform users of events relevant to them: "Your order has shipped", "Your appointment is in 30 minutes", "New message from Sarah". These have high value and users expect them.

Actionable notifications require response: "Approve expense request from John", "Delivery driver is waiting", "Two-factor authentication code". iOS and Android support inline actions that let users respond without opening the app.

Silent notifications wake the app in the background without displaying anything to the user. Used for triggering background sync or updating app state. Heavily throttled by both platforms; not reliable for time-sensitive operations.


Device Hardware Integration

Native apps can access device hardware that web applications cannot reach or can only access with limitations. The specific capabilities required should inform the platform decision.

Camera and image capture

Access to front and rear cameras with control over flash, focus, exposure, and zoom. Capture photos and video. Access raw image data for processing. Scan barcodes and QR codes. The web has basic camera access but lacks fine-grained control and performs worse on image processing.

Location services

GPS, Wi-Fi, and cellular positioning. Foreground location while the app is active. Background location for tracking applications (with significant battery impact and platform restrictions). Geofencing to trigger actions when entering or leaving areas. The web has foreground location only. For applications requiring sophisticated spatial features, see our approach to maps and location.

Biometric authentication

Face ID, Touch ID, and Android fingerprint authentication. Securely store credentials in the device keychain/keystore with biometric unlock. Users expect biometric login in apps that handle sensitive data. The web has WebAuthn but adoption is lower and the UX is less polished.

Bluetooth and NFC

Bluetooth Low Energy for communication with sensors, wearables, and peripherals. NFC for contactless interactions (iOS restricts NFC writing significantly). The web has partial Bluetooth support in Chrome but no NFC. If your application connects to hardware, native is required.

Sensors and motion

Accelerometer, gyroscope, magnetometer, and barometer data are available through native APIs. Use cases include fitness tracking, orientation detection, step counting, and augmented reality. React Native and Expo provide access through the expo-sensors package with a consistent API across platforms.

File system access

Native apps have a sandboxed file system for storing documents, cached data, and user-generated content. Files can be shared with other apps through platform-specific mechanisms (Share Sheet on iOS, Intent system on Android). Large file handling is more practical in native apps than web applications constrained by browser memory limits.


Authentication and Security

Mobile authentication presents different challenges than web authentication. Sessions persist across app restarts. Devices may be shared. Biometrics provide convenient but not infallible identification. Secure storage is available but must be used correctly.

Token storage

Authentication tokens must be stored securely. Both platforms provide secure storage mechanisms: Keychain on iOS and Keystore on Android. These encrypt credentials at rest using hardware-backed keys. Storing tokens in AsyncStorage, SQLite, or plain files is insecure and will fail security audits.

Common mistake: Storing refresh tokens in AsyncStorage because it is easier than using the secure keychain. This exposes credentials if the device is compromised or backed up insecurely.

Session management

Mobile apps typically use long-lived refresh tokens exchanged for short-lived access tokens. The refresh token persists in secure storage. The access token lives in memory and is refreshed as needed. When the app returns from background, it should verify the session is still valid before making authenticated requests.

Session expiration must be handled gracefully. If the user's session has expired, show the login screen with a clear explanation rather than generic error messages. Preserve the user's context so they can resume after re-authenticating.

Biometric unlock

Biometric authentication provides convenience, not identity verification. Face ID and Touch ID confirm that a registered biometric is present on the device, not that it belongs to a specific person. Use biometrics to unlock locally stored credentials, not as a substitute for server-side authentication.

The flow: user authenticates normally with credentials. The app stores the authentication token in the keychain with biometric protection. On subsequent launches, the user authenticates with biometrics to unlock the stored token. The token is still validated server-side.


Testing Strategy

Mobile testing is complicated by device fragmentation, platform differences, and the difficulty of simulating real-world conditions. A comprehensive testing strategy addresses multiple levels.

Unit and integration tests

Business logic and state management can be tested with standard JavaScript testing tools (Jest). These tests run fast, provide quick feedback, and catch logic errors before they reach devices. Aim for high coverage of business logic, lower coverage of UI glue code.

Component testing

React Native Testing Library enables testing components in isolation, verifying that they render correctly and respond to interactions. These tests catch UI bugs without requiring a full application build or device.

End-to-end testing

Detox (for React Native) or Appium run automated tests against the actual application on simulators and real devices. These tests verify complete user flows: login, navigation, data entry, submission. They are slower and more brittle than unit tests but catch integration issues that lower-level tests miss.

Unit tests
Fast, isolated, high coverage
Component tests
UI behaviour, interactions
E2E tests
Critical user flows
Device testing
Real hardware, manual

Real device testing

Simulators and emulators do not replicate real device behaviour. Performance characteristics differ. Hardware features may not work. Network conditions are idealised. Before release, test on actual devices representing the range your users have: recent flagships, mid-range devices, and older models still in use.

Test on devices with limited storage. Test with slow network connections. Test with the app backgrounded and resumed. Test with notifications arriving during use. The edge cases that simulators hide are the ones users encounter.


App Store Deployment

Publishing to app stores is a process with its own requirements, timelines, and failure modes. Both Apple and Google review submissions, though with different stringency and turnaround times.

Apple App Store

Apple reviews every submission manually. Review typically takes 24 to 48 hours but can extend longer for new apps or significant updates. Rejection is common and usually cites specific guideline violations. Familiarise yourself with the App Store Review Guidelines before submission.

Common rejection reasons: incomplete functionality, broken links, placeholder content, crashes during review, requesting permissions without clear justification, misleading metadata, non-functional login (provide test credentials).

Required assets: app icons at multiple resolutions, screenshots for each supported device size (iPhone, iPad if supported), privacy policy URL, app description and keywords, age rating questionnaire completion.

TestFlight allows distributing beta builds to testers before App Store submission. Use it to catch issues before they reach review.

Google Play Store

Google's review is faster (often hours rather than days) and more automated. Rejection is less common but policy enforcement has become stricter. Target API level requirements mean apps must be updated annually to meet new Android SDK minimums.

Required assets: similar to Apple but with different size requirements. Feature graphic for store listing. Content rating questionnaire. Data safety section describing data collection and usage.

Staged rollouts: Google Play allows releasing updates to a percentage of users, monitoring crash rates and feedback, then expanding. Use this for significant updates to catch issues before they affect all users.

Over-the-air updates

Expo's update system (expo-updates) allows pushing JavaScript bundle changes without app store submission. The app downloads the new bundle and applies it on next launch or immediately if configured.

OTA updates are appropriate for bug fixes, copy changes, and minor features. They are not appropriate for native code changes (which require a new binary) or significant feature additions (which may violate app store policies on functionality changes outside the review process).

Practical impact: A critical bug can be fixed in minutes via OTA rather than waiting days for app store review. This significantly reduces the stress and user impact of post-launch issues.


Performance and Battery

Mobile devices have constrained resources. Users notice when apps drain battery, consume data, or feel sluggish. Performance optimisation is not optional.

Render performance

Target 60 frames per second for all animations and scrolling. React Native achieves this by default for simple UIs but complex lists, heavy computation on the JavaScript thread, or unoptimised re-renders can cause frame drops.

FlatList configuration: use windowSize to control how many off-screen items are rendered. Use getItemLayout if items have fixed heights. Use keyExtractor properly. Avoid inline functions in renderItem.

Avoid bridge thrashing: the JavaScript-to-native bridge is a bottleneck. Minimise the frequency of bridge crossings. Batch updates. Use native driver for animations where possible (useNativeDriver: true).

Network efficiency

Mobile data is metered and connections are variable. Minimise payload sizes. Compress responses. Cache aggressively. Use pagination for lists. Implement request deduplication so the same data is not fetched multiple times.

Background network requests drain battery. Batch background operations. Use exponential backoff for retries. Respect system signals about connectivity changes rather than polling.

Battery impact

Location services, background processing, and frequent network requests drain battery. Users will uninstall apps that appear in battery usage reports. Monitor wake lock usage. Use significant location changes rather than continuous tracking when appropriate. Profile background behaviour.


Platform Differences

Cross-platform does not mean identical. iOS and Android have different conventions, capabilities, and user expectations. Respecting these differences improves user experience and reduces friction.

Navigation patterns

iOS uses edge swipe for back navigation. Android has a hardware/software back button. Tab bars are at the bottom on iOS, often at the top on Android. Modal presentations differ. Deep linking and app launching conventions differ.

React Navigation handles most of these differences automatically when using the appropriate navigators. Custom navigation implementations must account for platform conventions or risk feeling foreign to users.

Visual design

iOS favours rounded rectangles, blur effects, and a particular typography scale. Android Material Design uses elevation shadows, ripple effects, and different spacing conventions. A single design that ignores both platforms feels wrong on both.

React Native Paper and other UI libraries provide platform-adaptive components. For custom designs, use Platform.OS to apply platform-specific styling where conventions differ significantly.

System integration

Share sheets, file pickers, permission dialogs, and notification settings differ between platforms. Testing must cover both platforms, not just the one your development team uses. Automated testing should run on both iOS and Android.


Maintenance and Evolution

Mobile apps require ongoing maintenance beyond feature development. Platform updates, dependency updates, and app store policy changes demand regular attention.

Platform updates

iOS and Android release major versions annually. New versions introduce new capabilities, deprecate old APIs, and change permission models. Apps must be updated to support new OS versions (users expect it) and eventually to require minimum OS versions (as old versions become unsupportable).

Test against beta OS releases before they ship. Major iOS releases in September mean testing in July and August. Android releases are less predictable but betas provide advance warning.

Dependency management

React Native and Expo update regularly. Staying current avoids security vulnerabilities and ensures compatibility with new platform features. Falling behind makes each upgrade more painful. Establish a regular upgrade cadence rather than deferring indefinitely.

When upgrading React Native versions, review the changelog for breaking changes. Test thoroughly. Expo's upgrade command handles most of the work for managed workflow projects.

Monitoring and crash reporting

Production apps need crash reporting (Sentry, Bugsnag, or similar) to identify issues users encounter. Include breadcrumbs for context about what led to the crash. Set up alerts for new crash types and elevated crash rates. Monitor after each release.


What You Get

When a mobile app is the right answer, we build it with the architecture and practices described above. The result is an application that works reliably across platforms, handles real-world conditions, and can be maintained over time.

  • iOS and Android from one codebase React Native with Expo. Genuinely native UI, shared business logic.
  • Offline-first when needed Local database, sync queue, conflict resolution. Works without connectivity.
  • Push notifications that users value Proper permission flows, transactional notifications, deep linking.
  • Hardware integration Camera, location, biometrics, Bluetooth where your requirements demand.
  • Secure authentication Keychain storage, biometric unlock, proper session handling.
  • Tested across the matrix Unit tests, integration tests, E2E tests, real device testing.
  • Fast iteration post-launch OTA updates for JavaScript changes. Days become minutes.
  • Maintainable over time Clean architecture, managed dependencies, upgrade path.

Do You Need a Mobile App?

Answer a few questions to see if a native mobile app is the right choice.

Mobile App Decision Wizard

Build Your Mobile App

We build cross-platform mobile applications when a mobile app is genuinely the right answer. Native capabilities where you need them, efficient development where you do not. If you are unsure whether your project needs a mobile app or a web app, we can help you evaluate the trade-offs.

Discuss your mobile project →
Graphic Swish