Automating Play Store screenshots in Flutter

From a raw screenshot to the Play Store

After adding support for seven languages to my personal finance app, I very quickly discovered that making Play Store screenshots manually was just unworkable anymore. Every locale needs five or six screenshots and each screenshot needs to be in the right language. Additionally, I would have to do all of this tedious work again whenever I change anything significant to one of the views, some automated solution was necessary.

Automating screenshots

I used the Flutter “screenshots” package. To generate better screenshots, I used a demo SQLite database full of random transactions and I also fixed the clock time for the financial statistics pages to look similar months after months.

1
2
Config.clock = Clock.fixed(DateTime.parse('2019-06-07T04:34:10.958Z'));
Config.dbFilename = "https://.../demo.db"; // remotely fetching a demo database

The database is then downloaded at boot, copied into the application folder, this configuration clock is used everywhere time is gathered on the app.

1
static const supportedLanguages = ["en", "fr", "ru", "vi", "de", "es", "pt"];

The screenshot script then boots the application, loops into each supported language and supported currency per language, goes into each screen and creates a screenshot. The whole operation takes about 5 min for the 70 screenshots to generate.

A screenshot of the folder where all the phone screenshots are created, there's a lot of them visible on screen

All the raw captures are now generated.

Making those screenshots look better

I’ve created a second script which then takes the raw screenshots and transforms them into something nicer. Using a headless browser was a quick and easy option.

It was very easy to generate the kind of appearance I wanted by changing a few lines of css and the dynamic items (like the i18n text and the capture) could be replaced at runtime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const screenshotDirectory = path.resolve(`${__dirname}/../../android/fastlane/metadata/android/en-US/images/phoneScreenshots/`);
var files = fs.readdirSync(screenshotDirectory);

// ...
await page.goto(`file://${__dirname}/featuredView.html`);
// ...

for (var i = 0; i < files.length; i++) {
  const file = files[i];
  // ...
  await page.evaluate((screenshotDirectory, file, title, content) => {
      document.getElementById('screen_title').innerHTML = title;
      document.getElementById('screen_content').innerHTML = content;
      document.getElementById('main-image').style.background =
          `url(file://${screenshotDirectory}/${file})`;
  }, screenshotDirectory, file, i18nTitle, i18nDescription);
  await element.screenshot({ path: `generated/${file}` });
}

I’ve removed a few boring parts of loading the locale JSON files and plumbing logic which isn’t really that interesting but you get the idea.

And now the result we were waiting for…

An image of the phone application after adding some purple triangle background behind it. There is also now a title and a description on top. The application screen displays a list of categories along with the money spent that month on it.

Looks nicer isn't it?

Exactly the same screen as before, except everything is now in French instead of English

Also works for any supported language!

Sending all those screenshots to the Google Play Store

Uploading those screenshots to the Play Store is also a tedious task by itself, you need to go to each locale and add the screenshots one by one, I was sure we could do better.

Google provides an access to the Google Play Store api, although it’s much more limited than the actual Play Store in the browser, it’s good enough for my needs.

1
2
3
4
5
6
7
8
9
10
import { google } from 'googleapis';

const packageName = "fr.mavio";
const androidpublisher = google.androidpublisher("v3");

const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/androidpublisher'], });
const authClient = await auth.getClient();
google.options({ auth: authClient });

const { id: editId } = (await androidpublisher.edits.insert({ packageName })).data;

Now we’ve connected to the Google API and created a draft version (called “edit”) of our data. This API works in two steps, you make all the changes you want to the edit and then you commit the new data, similar to source control.

It’s now time to actually send the screenshots.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import * as fs from 'fs';

const googleLanguageToMavioMapping = {
    'en-US': 'en',
    'fr-FR': 'fr',
    'ru-RU': 'ru',
    'pt-PT': 'pt',
    'es-ES': 'es',
    'de-DE': 'de',
    'vi': 'vi',
};
const screenPath = `${__dirname}/../store-screenshots/generated/pixel3-root-`;
for (let language in googleLanguageToMavioMapping) {
    let file: fs.ReadStream;
    const mavioLanguage = googleLanguageToMavioMapping[language];
    var screens = ['accounts', 'stats', 'transaction', 'home', 'statspage'];
    for (var i = 0; i < screens.length; i++) {
        let type = screens[i];
        console.log(`uploading ${type} image screenshot for ${language}`);

        let currency = defaultCurrencyForLocale[language] || 'EUR';
        file = fs.createReadStream(`${screenPath}${type}-${mavioLanguage}|${currency}.png`);
        await androidpublisher.edits.images.upload({
            editId,
            packageName,
            language,
            imageType: 'phoneScreenshots',
            media: {
                mimeType: "image/png",
                body: file
            }
        });
    }
}

A few minutes of file upload later…

An screenshot of the Google Play Store. Seven languages are displayed and there is a list of phone screenshots for the current one selected.

The end result as seen on the Play Store

Here you go! All the captures for all the locales are now on the Google Play Store.

Conclusion

Automating screenshots generation for my app was a fun project. Doing everything manually becomes impossible when you support a few languages. Another benefit of the automation is that I can keep the Play Store up to date with any change I’m making in the app.

That happened to me once already, I’ve redesigned one of the statistics pages. Instead of spending a few hours to make the screenshots again for this page for each locale and currency, I’ve just ran the tools and everything was up to date once again!

Up Next

Solving 'cannot redirect socket on update/2' with Phoenix Live view components

One year of Flutter as a web developer

Looking back on a side project


Looking for More?

More tech articles are coming soon!