← Nesim Apps

App Store Screenshots Without a Phone: Robolectric, a Fake Image Loader and a Headless Browser

By Nesim Razon, independent Android developer · 2026-11-16

#Dev notes#Tools and calculators

My iPhone apps share their screens with the Android versions, written once in Compose. App Store Connect wants screenshots at 1206x2622 pixels, and I work on a Windows laptop with no iPhone to spare for the job and a limited amount of cloud build minutes that I would rather spend on real builds. So I let unit tests draw the screens. This is how it works, what broke, and what these images honestly are.

The idea: let a unit test draw the screen

Robolectric runs Android code on the desktop JVM, and with native graphics mode it can draw Compose to a real bitmap. Roborazzi saves that bitmap as a PNG. So a screenshot becomes a test: set up the screen, click through to the state I want, capture the root node, done. No emulator, no phone, no build service.

One qualifier does the sizing: w402dp-h874dp-xxhdpi. That is a 402 by 874 dp screen at three pixels per dp, which gives exactly 1206x2622 pixels, the size App Store Connect asks for. I checked the output files rather than trusting the arithmetic.

Safe areas by hand

An Android test window has no notch, so the top of every screen would sit under where the Dynamic Island goes. In the test I wrap the app in a box with 47 dp of top padding, which stands for the iPhone safe area. In one app I also added 34 dp at the bottom for the home indicator, because that app has a bottom navigation bar.

It is a rough stand-in, not a measurement. It only has to make the layout look the way it would look on the phone, with nothing hidden under the island.

Network images that never load

Robolectric has no useful network in these tests, so article covers and satellite pictures would show as empty boxes. Coil has a FakeImageLoaderEngine for this: I register real images, saved as test resources, against the URLs or URL patterns the app would request. The screenshot then shows real pictures and not placeholders.

The data gets the same treatment. For the news reader the feed in the test is a copy of the real feed taken from the live site. For the space app the daily data is a copy of the real daily file. I do not invent content for a store listing; I freeze real content.

Live content and endless loops

Some screens show things that change, like the current position of the space station. For those the test passes a fixed sample object, and the screen draws it as if it had just arrived.

That needed a change in the app code. The live screen polled in a loop, and a while(true) loop with a delay inside it can keep the test framework's waitForIdle from ever returning, because the app is never idle. I did not wait to find out whether it would hang; I added a preview parameter up front: when a sample is given, the screen shows it and does not start the loop. It is a small testing seam that also makes the screen easier to preview.

Screens with a WebView

Robolectric does not draw a WebView. The article view and the static pages in my news apps are WebViews, so the screenshot came out with a hole where the page should be.

The workaround has two halves. The test writes the page HTML, built by the same shared function the app uses, plus a screenshot of the screen without the page: just the app bar, which is 111 dp or 333 pixels high. A Python script then renders that HTML in headless Edge inside a 402-pixel-wide iframe at 3x scale and pastes the result under the app bar. The page is drawn by a real browser engine, the bar by Compose, and they meet at a fixed line.

The traps

Each of these cost me an evening or part of one.

What these images are, and are not

These are Android renderings sized for an iPhone. Fonts, scrolling feel, system bars and some small controls are not what iOS draws. I accept that for the store listing because the shared UI is the same code, but I do not call them iOS captures.

The actual iPhone behavior of those apps I checked separately, on an iPhone 7 installed through TestFlight. The screenshots show the screens; the real device told me whether the screens work.

Frequently asked questions

Does this produce real iOS screenshots?

No. They are Android renderings of the shared Compose UI, sized to 1206x2622. Real iPhone behavior was checked separately on a device through TestFlight.

Why is the size exactly 1206x2622?

The test uses a 402 by 874 dp window at xxhdpi, which is three pixels per dp, so the output is 1206 by 2622 pixels.

Why not take screenshots on a real iPhone or a simulator?

I have no spare device for it and no Mac, and I want to keep my cloud build minutes for real builds. Unit tests run on my own laptop for free and can be repeated at any time.

Why are WebView screens handled separately?

Robolectric does not draw WebViews, so the page HTML is rendered in headless Edge and pasted under a Compose-drawn app bar.

More from the blog