Blog Infos
Author
Published
Topics
, , ,
Published

A common thing for a mobile project is to present the look of the app to your users, for example by its screenshots at an app store. Production of such images is exactly the thing I have automated recently and what I would like to tell you about today.

How it was before:

  • During a release a set of screenshots had to be made manually by following a specific sequence of actions in the app;
  • All these screenshots had to be generated from 2 different locally running emulators in demo mode;
  • The very same set of screenshots had to be done with light and dark themes with a slight difference in system navigation mode;
  • Multiple locales support add another multiplier to the number of screenshots to make and to the overall complexity of the process.

How it is now:

  • Generating of the all above locally with a single command using a running emulator;
  • An on demand CI workflow that generates all the necessary screenshots from those 2 reference emulators.

Further processing of the screenshots is a bit out of scope of this article, as it varies from project to project.

High level solution

Quite a lot of steps are involved to get the final result. For my project the fastlane tool was picked to manage all the process. Fastlane is a great tool for automating certain actions for a mobile developer. Even if for some reason fastlane isn’t applicable for your project, you can still benefit from using some of its modules.

After you enable fastlane for your project, the screenshots making involves usage of 2 fastlane’s components: the screengrab action and the screengrab java library. Let’s see what they are responsible for.

Screengrab action

Very roughly this is a script that performs a sequence of shell commands, mainly communicating with an emulator via adb:

  • Uploading of a prebuilt app apk and test apk to an emulator;
  • Granting the necessary permissions;
  • Executing the instrumentation tests for each requested locale;
  • Pulling the screenshots from the emulator to the host machine and preparing an html overview file.

Yes, ad-hoc instrumentation tests are involved. But their purpose isn’t to be green or red, but rather to leave a side effect after their execution — the screenshots, which will be pulled by the screengrab action. Although it is possible to do a regular instrumentation tests and make screenshots at the same time, I would suggest having these things separate, unless you do a form of automated screenshot testing. Remember, the original goal was to produce the screenshots for the markets only.

It is convenient to have the configuration for the action in the Screengrabfile. It can have many parameters and some very basic are there right when you generate this file. Some important parameters will be mentioned further.

The screengrab action relies on app apk and test apk being already available, so it is convenient to create a separate lane in the Fastfile and use it instead of the action directly.

lane :screenshots do
  gradle(task: "assembleDebug assembleAndroidTest")
  screengrab
end

An important advantage of this action is that another fastlane action — supply — can upload these images to Google Play as new app screenshots.

Screengrab java library

A small helper java library for your screenshots-generating instrumentation tests. Prior the Version Catalog one would add it to the project as:

dependencies {
    androidTestImplementation 'tools.fastlane:screengrab:x.x.x'
}

Its features include:

  • Receiving a Locale from the screengrab action and setting up the emulator with it;
  • Optionally configuring the emulator’s demo mode (mainly the look of the status bar);
  • Making screenshots in a place, where the screengrab action will find them.

It is actually possible to use this library even without the fastlane, redoing the flow of the screengrab action in other way.

Fastlane tool, its screengrab action and the screengrab java library together do the heavy lifting for you. The only thing left is making a screen capture at the right moment in the app. Let’s see how to tackle all the requirements listed in the beginning of this article on the side of instrumentation tests.

Ad-hoc Instrumentation tests

Having the UI of an app fully in Compose grants a lot of neat features. For simple screenshots it isn’t that necessary to perform a sequence of actions to reach a particular screen or screen state. Just call the screen-level Composable function directly and supply the state you actually need to capture. Performing actions and making assertions is a prerogative of regular instrumentation tests.

Strictly speaking, a similar approach is applicable for Views, although the setup will be a bit more complex.

Minimal setup
@RunWith(AndroidJUnit4::class)
class ScreenshotsMakingSuite {

    private val uiDevice
        get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun homeScreen() {
        makeScreenshotOf("home") {
            HomeScreen()
        }
    }

    private fun makeScreenshotOf(
        screenshotName: String,
        content: @Composable () -> Unit
    ) {
        composeTestRule.activityRule.scenario.onActivity(
            ComponentActivity::enableEdgeToEdge
        )

        composeTestRule.setContent {
            AppTheme(content = content)
        }

        uiDevice.waitForIdle()

        Screengrab.screenshot(screenshotName)
    }
}

Using createAndroidComposeRule() is necessary here in order to get the reference to the enclosing activity and enable its edge-to-edge mode. Don’t neglect it, as targeting the upcoming Android 15 will enforce this mode by default.

Another key thing here is uiDevice.waitForIdle(). Most probably you would want to wait until all the rendering is completed before doing the screenshot. The composeTestRule actually exposes a method with the same name, but it occurs to be not 100% reliable. If you wait with composeTestRule.waitForIdel() instead, then sometimes you will get empty screens captured.

Another reason to use exactly the uiDevice.waitForIdle() is that you could have special side effects in your Composable theme, that set isAppearanceLightStatusBars and isAppearanceLightNavigationBars. This in turn can animate the colors of the status and navigation bars, which takes more time than Compose content to actually render. Setting the window_animation_scaletransition_animation_scale and animator_duration_scale global properties to 0 don’t solve the issue.

And even more, if you happen to configure the system navigation mode in your tests (gestural or 3 buttons), then a similar animation of the navigation bar takes place. Again, uiDevice.waitForIdle() is the solution.

Advanced arguments for Composables
// Somewhere in the app
@Composable
fun DetailsScreen(
    viewModel: DetailsViewModel = hiltViewModel()
) { ... }

class DetailsViewModel : ViewModel() {
    val data: StateFlow<String> = ...
}

// In ScreenshotsMakingSuite
@Test
fun detailsScreen() {
    val actualData = MutableStateFlow("Cheese!")
    val viewModel = mockk<DetailsViewModel>()
    every { viewModel.data } returns actualData

    makeScreenshotOf("details") {
        DetailsScreen(viewModel = viewModel)
    }
}

If your screen-level Composable requires ViewModel objects, whatever the means they are acquired normally, you can substitute them with mocks. We are in the instrumentation test, remember? Mockito or Mockk can do it for you. A good approach also is to have an intermediate fun DetailsScreen(data: String) in our case, which would accept data from the enclosing function with the ViewModel, but will not require mocking in tests.

Demo mode
@RunWith(AndroidJUnit4::class)
class ScreenshotsMakingSuite {

    @Before
    fun setUp() {
        CleanStatusBar()
            .setBluetoothState(BluetoothState.DISCONNECTED)
            .setMobileNetworkDataType(MobileDataType.LTE)
            .setWifiVisibility(IconVisibility.HIDE)
            .setShowNotifications(false)
            .setClock("0900")
            .setBatteryLevel(100)
            .enable()
    }

    @After
    fun tearDown() {
        CleanStatusBar.disable()
    }
}

The screengrab java library can control the content of the status bar via its CleanStatusBar class. It uses the Demo Mode for the Android System UI, wrapping the shell commands. It has some default values, but one may consider being more precise while setting the status bar’s look.

Putting this code in @Before and @After pair gives the expected result, in the contrast to @BeforeClass and @AfterClass.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

From Chaos to Consistency: Managing Build and Release for 25+ Android Repos with Github Actions

Managing the build and release process for over 25 Android repositories can be a daunting task. With each repository having its own pipeline or workflow, it can become difficult to ensure consistency and quality across…
Watch Video

From Chaos to Consistency: Managing Build and Release for 25+ Android Repos with Github Actions

Shrikant Ballal
Staff Engineer
YML

From Chaos to Consistency: Managing Build and Release for 25+ Android Repos with Github Actions

Shrikant Ballal
Staff Engineer
YML

From Chaos to Consistency: Managing Build and Release for 25+ Android Repos with Github Actions

Shrikant Ballal
Staff Engineer
YML

Jobs

https://github.com/Javernaut/WhatTheCodec/blob/main/app/src/androidTest/java/com/javernaut/whatthecodec/screenshots/ScreenshotsTestSuite.kt?source=post_page—–d8961ae19dca——————————–

One step further — doing it on CI

The final piece of requirements remains: using 2 exact reference devices to make the screenshots from. In order to free the developers from managing yet another 2 AVDs locally and for enabling the screenshots making for people who don’t need the development setup on their workstations, we can delegate the whole process to CI. Let’s see how a simplified solution may look like with Github Actions:

name: Making screenshots

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        device: [ "pixel_2", "pixel_6_pro" ]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Enable KVM group perms
        run: |
          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm

      - name: Executing tests for screenshot making
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          arch: x86_64
          profile: ${{ matrix.device }}
          disable-animations: true
          script: fastlane screenshots

      - name: Save screenshots
        uses: actions/upload-artifact@v4
        with:
          name: screenshots_${{ matrix.device }}
          path: fastlane/metadata/android

Key aspects:

  • The build matrix is used to do the whole process for 2 reference android emulators separately;
  • The reactivecircus/android-emulator-runner@v2 is used to launch the specific emulators. x86_64 architecture is used to match the host machine. Also, now this workflow encourages Ubuntu as the OS to run on, instead of macOS as it was before;
  • Executing the fastlane’s lane that assembles the apks and generates the screenshots;
  • Publishing of the screenshots as zip files, attached to the workflow run.

After the workflow run 2 zip files with all the necessary screenshots will await.

The workflow can (and should) be adjusted to more specific needs of a project. One may consider running this workflow as a part of the release process instead of a manual triggering. Also, in case of a bigger number for reference devices, one may think of splitting the workflow into multiple jobs, where only one would build the apks and the rest (and dependant) jobs would launch an emulator and generate the screenshots.

Conclusions

It takes a lot of time doing something manually, making mistakes and starting the process over again to fully understand the actual value of automation. Work smarter, not harder.

Thanks for reading. Cheers!

This article is previously pubished on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu