RSS    Archive    About

Automating app store graphics creation for Flutter apps

Creating app store graphics could take up a lot of your time and energy. Flutter widget tests can help you to generate these images. It might not be a typical way to use widget tests but if it works it’s not stupid, right?

Tree app store screenshot examples taken with the provided solution

Why do it with widget tests?

  • To save time. You don’t have to manually click through the app to reach the right screen and get it to the right state. Perfect screenshots can be taken every time for any app state needed. Automation helps saving and respecting other people’s time: designers don’t have to spend hours editing hundreds of screenshots just to add a device frame and localized text.

  • Flexibility. It works with any configuration: different screen sizes, locales, dark\light themes, text directionalities. Let computer do the boring work of setting configuration and taking screenshots.

  • Works for any configuration. No need to own various devices, no need to launch gazillions of emulators to make a screenshot of the right size. All screenshots can be taken on one platform.

  • Automation. You can create new screenshots after every new release, without having to manually repeat the process. This ensures that your screenshots are always up-to-date.

What do I do to make it work?

There are several steps to make it work. I’m going to use my simple coffee ratio calculator app called Ratio M8 as an example how to create graphics for Apple’s App Store. Code samples in this article are simplified for better readability. App is not open source but screenshot generator’s code is available on Github.

Step 1. Make app testable

This step depends highly on the way your app is built. You need to make your app testable, make it possible to show the app state for which you want to take a screenshot.

In my case it was easy because the app is very simple and uses Riverpod that allows overriding providers like this:

// Return custom state from shared preferences
// Ratio is 1:16.5, Beans: 24 grams, the rest is calculated
final model = MockPreferencesModel(16.5, 24);

await tester.pumpWidget(
    ProviderScope(
        overrides: [
            preferenceModelProvider.overrideWithValue(model),
        ],
        child: const App(),
    ),
)

Sometimes it’s enough to simply pump a widget you want to take a screenshot of. Sometimes you need to have a widget test that goes through the app and clicks various items. And sometimes you need to take care of your platform-specific dependencies.

Step 2. Show actual texts

As you probably know, Flutter widget tests use a different font from what you use in the app. If you create a screenshot from what’s on the screen without this step it would look like this:

App screenshot with all letters replaced by black rectangles

The trick is to make the app in your tests look nicer by loading actual fonts before running tests:

final fira = rootBundle.load('assets/fonts/fira_code_regular.ttf');
final loader = FontLoader('Fira')..addFont(fira);
await loader.load();

Text widgets that use this font family will now be rendered correctly.

Optional: add device frame and add text

Before taking a screenshot, it’s possible to wrap app into a device frame, add text or other content and apply effects and transformations.

In this example I simply wrap the app widget into an iPhone 13 frame using device_frame package and add a caption:

ColoredBox(
    color: Colors.white,
    child: Column(
        children: const [
            Text('This is some caption'),
            DeviceFrame(
                device: Devices.ios.iPhone13,
                screen: App(),
            ),
        ],
    ),
),

Step 3. Take a screenshot

Last touch before taking a screenshot is to add RepaintBoundary widget around the test app:

RepaintBoundary(
    key: rootKey,
    child: screenshotContent,
),

Finally a screenshot can be taken and saved to a file:

final boundary = tester.renderObject(find.byKey(rootKey));
final image = await boundary.toImage();

final file = File('my_image.png');
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
await file.writeAsBytes(byteData!.buffer.asUint8List());

Run the test and voilà! Screenshot including device frame and everything inside RepaintBoundary is saved as my_image.png.

Notes

  • Navigation bar, status bar and other platform UI elements are not captured and have to be added as an image on top of the app screenshot if needed. This could be a part of the screenshot generation code.

  • This solution most likely doesn’t work with Flutter’s platform views.

  • As any other test and automation, some initial work has to be done to make it work.

  • Similarly to other tests this code needs to be maintained.

Conclusion

It may seem easier to use a real device or emulator to create screenshots rather than spending hours writing code. It might be true if you take screenshots once when app is initially released. In a long run with constantly updating configurations and app changes, it’s worth investing into automation like this.

App store images don’t have to be frame-and-text-boring as shown in the example above. You can create more engaging images. For example, a screenshot embedded into an isometric device frame using Flutter’s Transform widget:

Example of a fancier screenshot: app screenshot is embedded into an isometric device frame

Flutter app with a touch of Material You colors, please

Android 12 brings the third iteration of Material Design called Material You - along with other changes apps and home screen widgets may now change their look according to wallpaper colors or selected system palette.

There are no design guidelines for using these new colors although Google is updating their apps - Gmail, Keep, Phone, Calendar and others have already got their Material You makeover. It feels a bit like a secret - there are no guidelines but apps are getting updated. Unfortunately, Flutter has no support for the new coloring system yet.

As a developer I want to support and utilize the latest available specifications and APIs. Let’s get the new colors in Flutter.

Getting colors using a platform channel

The idea is simple - call a “native” platform code to get a list of colors. The colors we’re interested in are listed in Dmitry Chertenko’s article and in the official documentation. There are several new system color attributes available:

system_accent1_0
system_accent1_10
system_accent1_50
...

There are three groups of accent colors and two groups of neutral colors:

Each color group has 13 shades and looks like a gradient - it starts with a lighter color and ends with a darker one. Switching between light and dark system modes does not change the palette’s colors.

Note: make sure to set targetSdk to 31 in app’s build.gradle file. Otherwise the new R.color.* attributes are not accessible.

Making a call via platform channel is trivial so here’s the code.

The map of colors in Flutter’s MaterialColor is slightly different from what Android offers - Flutter expects to have a primary color and its 10 shades, Android provides 13 shades. I simply ignored some of the shades on the Flutter side although it doesn’t feel right - some of the ignored colors could be useful (see palette usage below).

Guessing Material You colors usage

As there are no guidelines on how to use the colors. I’ve used a modern nanotech tool (a simple color picker) to find matches between my current Material You palette and real apps. This may help in understanding how colors should be used.

Calculator

Calculator uses colors from all three accent color groups. I couldn’t find a color matching digit button’s background - perhaps it’s a color with opacity and it blends in with window background which makes it’s hard to get the actual color.

Gmail

Search and navigation bars use changed version of palette’s colors. My guess is they use a transparent color and a solid color background container (white for light theme and black for dark theme) to make the final color opaque. Let’s see what Material You guidelines say about it once they’re published.

Gboard

Pretty much every color is from the palette:

There’s no clear logic which color to use in each case. Looks like the way to achieve colorful Material You looking apps is to use shades 100-300 of accent colors for main controls and lighter\darker colors for the rest of the items.

Using Material You colors in Flutter

Using colors in Flutter is simple: get a color and set it as a primary swatch:

FutureBuilder(
  future: getMaterialYouColor(),
  builder: (context, AsyncSnapshot<MaterialYouPalette?> snapshot) {
    final primarySwatch = snapshot.data?.accent1 ?? Colors.blue;
	return MaterialApp(
	  theme: ThemeData(
	    primarySwatch: primarySwatch,
...

The biggest challenge comes with widgets customization. Setting a primary swatch is simple but overriding themes for all needed widgets could be hard - it’s a lot to do, to test and to maintain. And then there’s dark theme. And fallback theme for other platforms where Material You colors are not supported. Let’s hope Flutter will have a better and simpler solution for that.

In this article I’m looking for a way to bring Material You colors to Flutter. Can it be done? Yes. Is using these colors to style widgets a pleasant experience? Doesn’t feel so.

Eyecandying Android app's splash screen with animations

Usually when you see an animated splash screen tutorial for Android it shows how to build a splash screen that takes some (or a lot) of user’s time just to show a cool animation app developers built without any particular reason.

This article shows how to add an animation to seamlessly land your user from your static splash screen on your app’s first screen. It’s not only adding an animation to your app but showing app content by going “through” the splash screen. User may have a feeling of entering your app’s gates. A good example where this animation can be used is a first time use when onboarding a new user.

A short GIF is worth a thousand words:

Things to note in the video above:

  • Splash screen is displayed immediately after app start. There’s no blank screen between clicking the app icon and showing the actual content
  • Time before animation starts is the time that the operating system needs to show first app’s screen with the actual content
  • Splash screen has a very simple and quick animation to show first screen of the app with the actual content.

With great power comes great responsibility: it’s easy to ruin user experience by building something useless. Don’t waste user’s time and think twice when building something like this.

Here’s a step by step diagram:

The trick is to seamlessly change static splash screen to an activity with the actual content. The steps are:

  • Have a properly implemented splash screen (read what “proper” means below)
  • Open main app activity as soon as possible without activity transition
  • Have a special overlay view placed in the main activity that is perfectly aligned with splash screen’s image. This overlay view shows the actual animation. The animation can be done using Android’s animation framework but I preferred a lower level animation so there was more control over the animation.

Next sections share some of the implementation details but there’s nothing special about it. Standard components are used and things are done in standard ways. Complete example is available on Github.

1. Build a proper static splash screen

Building a proper splash screen is easy. “Proper” in this case means:

  • being able to be shown immediately after user clicks the app icon
  • not doing any heavy processing: no database queries or network requests.

Building a splash screen like this has been covered many times in other articles. For example, this article can be used as a tutorial. The implemented splash screen is so lightweight it doesn’t even call setContentView().

This splash screen works extremely good itself without even building what’s being built in this article.

It looks especially good when it matches app icon’s background and icon as Android is showing an icon-to-app startup animation out of the box. Launchers behave differently so app-to-icon animation may not work on some devices. You know, things are different on Android ¯\_(ツ)_/¯

2. Seamlessly open MainActivity

In this section by SplashScreenActivity and MainActivity I mean Android activities for the static splash screen and the first screen with actual app content.

SplashActivity is seamlessly changed to MainActivity. User is not able to say when a new activity is opened because both activities look exactly the same: same background color, same logo size, same logo position. First frame of MainActivity’s animation must match how SplashScreenActivity looks. Achieving seamlessness is easy:

  • There’s an overlay view placed on top of everything in MainActivity. This view draws logo, background and animates them when time comes. It’s the key part of the seamless transition
  • Disable animated transition between SplashScreenActivity and MainActivity so it’s visually impossible to see when next activity is displayed:
// SplashScreenActivity.kt
startActivity(Intent(this, MainActivity::class.java))
overridePendingTransition(0, 0)
finish()

User lands on a regular activity with a layout - there’s full control over what’s displayed and how it’s animated. I personally like to use short scale and alpha animations to transition between splash screen’s logo and actual app content.

Using vector graphics for animated logo is important as the scaled logo’s edges should look sharp when scaled many times. I’m using Android’s vector drawable converted to Path as shown in my previous post. I ended up using a custom view with a Path after several attempts using other features of the framework:

  • Shared element transition is not really suitable and hardly customizable
  • Scaling vector drawable with scale() is not working well either due to bounds issues when animation can’t go outside bounds.

Notes

  1. I like building apps with edge to edge design so animation is going to infinity and beyond and nothing can stop it (status and navigation bars in particular).

  2. Complete code example is available on Github. Key files:

  • SplashScreenActivity - very simple splash screen with a background and a logo. Simply starts main app activity from onCreate without inflating a layout
  • MainActivity - activity for showing app content, takes part in seamless transition from splash to content by hosting an overlay view
  • SplashView - an overlay view used for seamless transition between splash screen and main activity and for animating the splash screen.

Getting Android vector drawable as Path

You ever wondered how to get a Path from Android’s vector XML drawable to draw it on canvas? Here are some options.


Getting a vector drawable and drawing it on a canvas with any size is easy:

val vectorDrawable = ResourcesCompat.getDrawable(resources, R.drawable.icon, null)

vectorDrawable?.setBounds(0, 0, 800, 800)
vectorDrawable?.draw(canvas)

This produces a vector drawable which is properly scaled and looks sharp in most cases. Although if you scale this image too much some artifacts may appear, scaled icon may become blurry. Look how sharp the text is and how pixelated the scaled shape is:

In case you want to have better scaling and implement advanced features with paint, gradients and shaders you might need to get a vector drawable as instance of Path.

Understanding XML path data

Each vector XML file contains instructions how to draw it. Compare parts of XML and Kotlin representation of the same icon:

<path
    android:fillColor="#FF000000"
    android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,..."/>
path.moveTo(17.6f, 9.48f)
path.lineTo(19.44f, 6.3f)
path.cubicTo(19.6f, 5.99f, 19.48f, 5.61f, 19.18f, 5.45f)

This XML drawable contains path instructions which can be mapped into a set of Path calls:

  • M is translated to moveTo
  • l is translated to lineTo
  • c is translated to cubicTo

Uppercase is for absolute coordinates, lowercase is for coordinates relative to previous instruction. Thus, l1.84,-3.18 following M17.6,9.48 becomes lineTo(19.44f, 6.3f). Quick maths:

  • First argument: 17.6 + 1.84 = 19.44
  • Second argument 9.48 - 3.18 = 6.3

It’s possible to manually build a Path object that matches instructions of an XML drawable. Although it’ll probably take a lot of your time. It’s also easy to make a typo.

Read more about XML path data in this awesome article.

Generating Path code from vector image using an online generator

codecrafted.net/svgtoandroid is online generator that converts an SVG file into Java code, similar to what was demonstrated in the previous section. Resulting code can be optimized and improved by converting it to Kotlin and checking that all variables have meaningful names. Always think twice before uploading any of your app code including SVG icons into 3rd party services.

Converting vector XML into Path programmatically

A Path can be created from a String containing path instructions. There are two main steps to convert a vector drawable into Path:

  1. Parse content of a vector XML drawable and extract pathData attribute from it
  2. Create new Path object from path data

This gist shows how you can implement a parser for a simple XML with one path node. Getting a Path from path data string is trivial:

import androidx.core.graphics.PathParser

PathParser.createPathFromPathData(pathData)

Look how sharp this shape is after scaling a Path using Matrix:

A complete example will be provided later as a part of a bigger project.

Getting rid of generated files in search dialog in Android Studio

This is an old post. It was originally posted on Medium

Have you ever seen a picture like this when looking for a file or a class in Android Studio?

It’s populated mostly by unrelated generated files. They’re always blocking you from finding the right one. Luckily, there’s a way to tell Android Studio (or any flavor of IntelliJ IDEA) to ignore these files.

My first idea was to make Android Studio ignore the build folder but it didn’t work properly when you needed to reference BuildConfig or any other generated class.

Another way of fixing the issue would be getting generated file names, creating name masks and ignoring them. This is what I found for our project.

Realm:

*RealmProxy.java
*RealmProxyInterface.class
*RealmProxyInterface.java

Dagger:

*_MembersInjector.java
*_Factory.java
_Provide*Factory.java
*Module_*.java

Be careful with the last one. Our Dagger modules are called like CommonToolsModule.java so we can easily ignore all the module-related generated files using that mask.

ButterKnife:

_ViewBinding.java

Parceler:

*$$Parcelable.java

Add these masks to PreferencesEditorFile TypesIgnore files and folders field as semicolon-separated values. Applying these changes will affect File… and Class… dialogs, search dialog and usage search.

There’s one more little guy that annoys me every time I’m looking for a string in a project using Find in path or Replace in path dialogs. Its name is lint report. Let’s add lint-report.html to the list of ignored files too.

The final configuration looks like this:

*RealmProxy.java;*RealmProxyInterface.class;*RealmProxyInterface.java;*_MembersInjector.java*_Factory.java;_Provide*Factory.java;*Module_*.java;*_ViewBinding.java;*$$Parcelable.java

And the result is: