Native Android

Android App Development in Kotlin and Jetpack Compose

Android is not one device, one manufacturer or one set of rules. We build for the phones people actually own — including the cheap ones, the ones with an aggressive battery manager, and the ones running an OS release from three years ago.

  • Device matrix agreed up front
  • Staged rollouts, always
  • Play Console in your name
An Android developer comparing the same app build running on several different handsets
The Stack

Kotlin and Compose, Written So It Stays Fast on Cheap Hardware

Kotlin with coroutines and flows for everything asynchronous, Jetpack Compose for the interface, and the Jetpack libraries underneath rather than a homegrown framework somebody has to reverse-engineer in two years.

The part that gets skipped is what happens after the code compiles. Compose is declarative, which makes it easy to write a screen that redraws far more than it needs to. On a flagship you will never notice. On a two-hundred-dollar handset with slow storage it is the difference between a list that glides and one that stutters every time you scroll.

So we treat performance as something you measure, not something you hope for.

Recomposition kept honest

State lifted to the right level, stable types so the compiler can skip unchanged parts of a screen, keys on list items, and images decoded at the size they will be drawn rather than at full resolution. Most Compose performance complaints trace back to one of these four.

Cold start budgeted

A baseline profile shipped with the app tells the runtime which code paths to compile ahead of time, which measurably cuts the time to first frame on lower-end devices. We set a cold start target at scoping and test against it rather than discovering it in a review.

Minification that does not surprise you

Release builds are shrunk and obfuscated, which is good for size and bad for anything using reflection. Serialisation models and third-party SDKs need rules, and the failure mode is a crash that only ever happens in release. We test the release build, not just the debug one.

Coroutines tied to lifecycle

Work started by a screen stops when that screen goes away, and data collection pauses when the app is not visible. Getting this wrong is how an app quietly holds a network connection open in the background and turns up in the battery settings screen.

Views where Views still win

Compose and the older View system interoperate in the same app, so a mature map, chart, video or camera component that only exists as a View gets embedded rather than rebuilt badly. Rewriting a working component to be consistent is rarely worth what it costs.

Local data that survives

A proper local database with versioned migrations, so an update does not wipe what the user had. Offline behaviour is designed rather than bolted on, because the connection on a train is the normal case, not the edge case.

Material Design

Material Is the Floor, Not the Ceiling

Material tells you what an Android user already expects: where navigation sits, what a long press does, how a sheet dismisses, what the back gesture means. Start there. Then decide, explicitly, which parts your product is going to overrule.

Keep the behaviour

Things Android Users Have Already Learned

  • The system back gesture, including the preview of where it will take you
  • Edge-to-edge layouts that respect the status bar and gesture inset properly
  • Notification channels, so people can silence one kind of alert without silencing all of them
  • Font scaling and display size, which a lot of Android users genuinely do change
  • Dark theme as a real theme, not an inverted screenshot
  • Standard share, selection and long-press menus rather than a private invention

Overrule on purpose

Where a Product Should Look Like Itself

  • Your palette and type, held consistent, rather than a system-generated one
  • Dynamic colour, which recolours the app from the user wallpaper, is lovely for a utility and wrong for a brand that has to stay recognisable
  • Custom navigation when the standard bar cannot carry the structure you actually have
  • Shape, elevation and motion tuned to your product rather than accepted wholesale
  • Bespoke empty, loading and error states, which is where most apps look cheapest

One rule decides most of these arguments: change what it looks like freely, change what it does reluctantly. Design work for both platforms is covered on app UI and UX design.

Fragmentation

The Problem Is Not Screen Sizes. It Is That the Rules Change Underneath You.

Different screen dimensions are a solved problem. What is not solved is that two phones on the same Android version, from two manufacturers, can run identical code and behave differently.

What actually varies

  • How hard the battery manager restricts an app the person has not opened lately
  • Whether a scheduled job survives a reboot, or whether the launch-on-boot signal is withheld
  • How much memory a backgrounded process keeps before it is reclaimed, which decides whether the app resumes or restarts
  • Camera behaviour, particularly orientation metadata and what comes back from a capture on low-end hardware
  • Notification presentation, grouping and whether a channel importance is honoured
  • Storage speed, which turns an acceptable database query into an unresponsive screen
  • Default font scale and display size, which some manufacturers ship larger than stock

How it is actually tested

  • A written device matrix agreed at scoping, not an open-ended promise to test everything
  • Always: one deliberately cheap handset, one current flagship, the oldest OS version in scope, the newest, and at least two manufacturers
  • Emulators for layout and OS version sweeps, physical devices for anything involving battery, camera, sensors or real network conditions
  • A cloud device farm for the long tail, where a run across many models costs less than owning three of them
  • Automated pre-launch runs on every upload, which install the build on real devices in a lab and return crashes, screenshots and accessibility warnings
  • Deliberately bad conditions: flight mode mid-request, a throttled connection, a nearly full disk, permissions revoked from Settings while the app is open

Crash rate

Collected from real devices and reported back per model and per OS version, so a crash confined to one manufacturer is visible rather than anecdotal.

ANR rate

Frozen main thread events. Users almost never report these, Google always does, and they are far more common on slow storage than on a developer handset.

Excessive wakeups

The metric that catches an app waking the device too often. It is the difference between a background feature and a battery complaint.

Startup time

Measured in the field rather than on your laptop, which is the only measurement that reflects the phones your users actually carry.

Our full approach to device coverage, automation and release gates is on app testing and QA.

Background Work

Most Reports of a Broken Sync Are Android Doing Its Job

Android spends a great deal of effort stopping apps from draining batteries, and manufacturers add more on top. An app that assumes it can run whenever it likes will work perfectly in development and fail quietly for real users.

What the system does to you

  • Once the screen is off and the device is still, network access and alarms get batched into occasional windows instead of running on demand
  • Apps are sorted into buckets by how often the person opens them, and rarely used apps are restricted far harder than daily ones
  • An app that has been force-stopped stays stopped until the person opens it again, which includes losing scheduled work
  • Starting work from the background without a user-visible reason is restricted, so an old-style background service quietly refuses to launch
  • Alarms that must fire at an exact minute are a separate, justified capability rather than something you simply use
  • Manufacturer battery managers go further than stock Android and are the single most common cause of a feature that works on one phone and not another

What we do about it

  • Deferrable jobs expressed with constraints: run when charging, run on an unmetered connection, retry with backoff, survive a reboot. The system picks the moment, not us.
  • Work the person has asked for right now runs in a foreground service with a visible notification and a declared, honest service type. Using one to dodge the limits is a policy violation, not a workaround.
  • Push messages to wake the app when something changes, instead of polling on a timer that will be throttled anyway
  • Notification permission asked at the moment it makes sense, not on first launch, because the acceptance rate is not close
  • Where a feature genuinely needs an exemption from battery optimisation, a clear in-app explanation and a direct route to the setting, rather than a silent failure
  • Nothing important ever assumes background work has already happened. Every screen can fetch what it needs when it opens.

Background location, all-files access and similar sensitive permissions need a written justification to Google and sometimes a demonstration video. If the feature does not clearly require it, removing the permission is usually cheaper than defending it.

Play Console

Releasing to Everyone at Once Is a Choice, and Usually the Wrong One

The best thing about shipping on Android is that you do not have to ship to everybody. Each release moves through tracks, and the production track itself goes out in slices.

Internal testing

Your team and ours. Builds available within minutes of upload, no review step, used for the daily sprint build.

Closed testing

An invited group by email list or link. Also the track that satisfies the testing requirement on a new personal developer account.

Open testing

A public beta anyone can join from the listing, plus the automated pre-launch report across real lab devices.

Production, staged

A small percentage of users first, increased as crash and ANR rates hold, halted the moment they do not.

Halting is not rolling back

Version codes only move forward, so you cannot republish yesterday. Halting a rollout stops anyone else receiving the bad build; fixing the people who already have it means shipping a higher version with the old behaviour back in it.

In-app updates

The app can detect that a newer version exists and either offer a background update or, for a build that fixes something serious, insist on it before continuing. Worth wiring in early, because you only want it the week you already have a problem.

The first hours are for watching

A rollout is not finished when it starts. We watch crash-free sessions, ANR rate and the reviews arriving on the new version before widening the percentage, and we widen it in more than one step.

Release notes per language, country availability, pricing and the rollout percentage are all per release, and we run all of it inside your Play Console account rather than ours. Listing, screenshots and keywords are covered on App Store Optimisation.

Bundles and Signing

The One Android Decision You Cannot Undo

Nearly everything on an Android project can be changed later. Signing is the exception, which is why it gets set up properly in the first week rather than the week of launch.

App bundles, and what they save

What you upload to the store is an app bundle: everything for every device, in one artefact. What a user downloads is an APK that Google generates for their specific handset, containing only the screen density, the languages and the processor architecture that device actually needs.

That difference is not cosmetic. Install size is one of the clearest predictors of whether somebody on a metered connection or a nearly full phone finishes installing your app at all, and the bundle usually removes a meaningful share of it without you changing a line of code.

Plain APKs still matter, just not for the store: direct installs, internal distribution, device fleets managed by a company, and testing a build on a phone that is sitting on the desk.

Play App Signing, in plain terms

There are two keys, and people conflate them. The app signing key is what every installed copy of your app is signed with, and it can never change for the life of that listing. The upload key is what we sign builds with before sending them to Google.

With Play App Signing, Google holds the app signing key. If an upload key is ever lost or compromised, it gets reset and no user notices. Without it, losing the key historically meant never updating that listing again, publishing a new one and asking every existing user to reinstall from scratch.

Both live under your Play Console account, created in your company name in week one. We hold access, not ownership, and handing over means handing over nothing, because it was never ours.

Policy

Play Obligations Do Not Stop When You Launch

Apple review is an event. Play compliance is a subscription. These are the obligations that keep running for as long as the listing exists, and they are the main reason an Android app cannot be left alone for two years.

The target API level rule

  • Play requires your app to be built against a recent Android release, and enforces it on a rolling schedule
  • Miss it and you eventually cannot publish updates at all; leave it long enough and the listing stops being offered to newer devices
  • Each bump brings real behaviour changes, usually around permissions, background work and storage, so it is a small project rather than a version number edit
  • We treat it as a planned maintenance window every year, the same way we treat the autumn OS releases

The Data Safety form

  • A public declaration of what you collect, what you share, whether it is encrypted in transit and whether deletion can be requested
  • It has to describe what the code and every third-party SDK actually do, not what the marketing page says
  • We complete it from the dependency list and hand it to you to approve, then revisit it whenever an SDK is added
  • It also has to stay true, which means an added analytics tool is a compliance change as well as a technical one

Accounts and deletion

  • If people can create an account in the app, they must be able to delete it from inside the app
  • There must also be a route for somebody who has already uninstalled, which in practice means a web page
  • It is a backend endpoint, a confirmation flow and a decision about what is genuinely deleted versus what you are obliged to retain
  • Frequently missed, and always at the least convenient moment

Testing before production access

  • New personal developer accounts must complete a period of closed testing with a group of real testers before applying to publish publicly
  • Google sets the group size and the duration and has changed them before, so we confirm the current rule at kickoff rather than working from a number in an old blog post
  • It is a schedule item, so it starts alongside the build and runs in parallel rather than blocking launch
  • Publishing under a verified organisation account instead of a personal one avoids it, and there are separate reasons to prefer one anyway

Ongoing target API bumps, policy changes, library updates and OS releases are exactly what a support plan is for. An unmaintained Android app does not stay still; it stops being publishable.

The Honest Bit

When Native Android Is the Wrong Choice

We build native and cross-platform and have no stake in the answer, so here is the version we give on the phone.

Look at cross-platform instead

If Any of These Are True

  • iOS is needed at the same time and there is one budget for both
  • The app is screens, lists, forms and API calls rather than sensors and hardware
  • You are still proving the idea and speed to market beats polish
  • One small team will maintain it for years and cannot staff two codebases

Native Android earns its cost

If Any of These Are True

  • Sustained background work, precise location or Bluetooth is the product
  • Your users are on low-end hardware where every frame and megabyte counts
  • The app runs on managed device fleets, kiosks or rugged handsets
  • Widgets, quick settings tiles, deep system integration or Wear OS matter

A simplification of a longer conversation, and the recommendation depends on your app rather than on a rule of thumb.

FAQ

Android Questions We Get Asked

What happens if we lose the app signing key?

With Play App Signing, very little, which is the whole reason we use it. Google holds the key that signs what users install, and we upload builds signed with a separate upload key. If the upload key is lost or compromised, it can be reset, and your users never notice. Without Play App Signing the answer is much worse: a lost key historically meant you could not update that listing ever again, and you would have to publish a new one and ask every user to reinstall. This is one of the few decisions on an Android project that is genuinely irreversible, so we set it up correctly in week one and the keys live under your account, not ours.

Why does the app behave differently on phones from one particular manufacturer?

Because several manufacturers ship their own layer on top of Android and it changes rules your code depends on. The most common difference is aggressive battery management that suspends or kills apps the person has not opened recently, which stops scheduled work, delays notifications and drops listeners that stock Android would have kept alive. Others differ in how notification channels are presented, how permissions are surfaced, how the camera stack reports orientation, and how much memory a background process is allowed before it is reclaimed. None of it shows up on a clean developer handset. We handle it by testing on devices from more than one manufacturer, by never making a feature depend on background work having run, and by giving users a clear path to whitelist the app when a feature genuinely requires it.

Can we roll back an Android release that goes wrong?

Not in the way people expect. You cannot re-publish an older version over a newer one, because version codes only move forward. What you can do is halt a staged rollout immediately, which stops new users receiving the bad build while everyone still on the old one stays there. To fix the people who already updated, you ship a new build with a higher version code containing the old, working behaviour. That is why we roll out in stages rather than to everybody at once, why serious features sit behind a server-side flag that can be turned off without a release, and why the first hours of a rollout are spent watching crash and ANR rates rather than celebrating.

What is an ANR, and why does Google care about it more than we do?

An ANR is an Application Not Responding event: the main thread was blocked long enough that the system offered the user the option to close the app. Users rarely report these, because from their side the app simply froze and they gave up. Google collects them from real devices and publishes your rate back to you alongside your crash rate, holds both against a quality threshold, and factors them into how the listing is surfaced in the store. The usual causes are ordinary: a database query, a file read, a large image decode or a network call on the main thread, or a broadcast receiver doing too much. They are also far more common on cheap hardware with slow storage than on the phone in the developer pocket, which is exactly why the test matrix includes a low-end device.

Do we have to run a closed test before we can launch?

If the app is published from a newly created personal developer account, then yes: Google requires a period of closed testing with a group of real testers before that account can apply for production access. The exact size of the group and the length of the period are set by Google and have been adjusted before, so we confirm the current requirement at kickoff rather than quoting a number that may have changed. Two practical consequences. First, it is a schedule item, so it starts alongside the build rather than after it. Second, publishing under a properly registered organisation account instead of a personal one avoids it, and there are other good reasons to do that anyway.

Should we build for tablets and foldables, or just phones?

Phones first, almost always, but the decision has to be made rather than drifted into. A phone layout does run on a tablet, it just looks like a stretched phone layout, and on a foldable it has to survive being resized mid-use when the device opens or closes. That last part is not optional even for a phone-only app: if the app restarts or loses what the person typed when the screen changes shape, that is a bug on ordinary phones too, because rotation and split screen do the same thing. So we always build the state handling that makes resizing safe, and we treat proper large-screen layouts, with a list and detail side by side and keyboard support, as a scoped and priced addition rather than a free side effect.

Get an Android Build Scoped Against Real Devices

Tell us what the app has to do and who carries the phones. You get a written scope, a device matrix and a cost range back within two business days.