Ribbonize your Android application laucher icon (again)

Mateusz Kwieciński
6 min readSep 2, 2020

--

a story of an ongoing attempt to reboot existing Gradle plugin that adds ribbon to your Android app launcher icon.

Sample icon overlay generated by the plugin

It is available on Gradle Plugin Portal at:

plugins {
id "com.starter.easylauncher" version "3.1.0"
}

+ its source code 👀

History

Having a ribbon attached to the app launcher icon on Android doesn’t sound as something unusual. There are at least several reasons why one might want to do so. Either they want to make debug or QA builds more distinguishable having multiple app variants installed side by side. Or to let user know they’ve been using an app from a specific release track (alpha, beta, etc.)
Android Gradle Plugin allows developers to define multiple launcher icons depending on app build variant by placing different icon resources in corresponding source sets. For example src/debug/res/mipmap/ic_laucher.xml and src/release/res/mipmap/ic_laucher.xml to use different icon in debug and release builds. See the documentation for reference.
Such approach might become hard to maintain and difficult to understood by other developers when the app configuration consists of multiple product flavors and build types.
Fortunately in OSS friendly Android environment, mindful people created Gradle plugins that automate that process (at least in most use cases):

Unfortunately they both are not maintained anymore.

Given all of that, still having a need for an easy way to distinguish different build variants in the app I’ve been working on, I decided to breathe a second live to Akaita’s plugin. In addition, here in this post, I wanted to share some context and thoughts from all the work I have done.

Challenges

The thing worth doing first was to identify all outstanding tech debt that prevented (presumably) many developers from using the library.
Going hrough all opened issues helped to identify a few crucial issues present in the last released plugin version:

Vector drawables support

The main eye-catching issue was missing support for Vector drawables, which are common way of defining the app icon. Usually it goes together with Adaptive definition, used both as its background and as well as a foreground.
Before jumping into details let’s discuss the base mechanism which the plugin bases on.

Core mechanism

When the plugin gets applied to the project, it creates Gradle tasks per each build variant which looks up in corresponding source set for the app launcher icon referenced in AndroidManifest.xml file, excluding xml files during the lookup.
Having a reference to all launcher icons for a specific build variant the plugin read the image into a memory, applied user-provided customizations and lastly saved a copy under generated resource. The key part was the directory where it saved a transformed image under - the most build variant specific resource directory. Assuming existing icons had been placed under main/res/mipmap directory, and the selected build variant was i.e stagingDebug - the directly with transformed image would be stagingDebug/res/mipmap/, leaving the rest to Android Gradle Plugin, which picks the most specific resource when merging them in compile time. Neat, eh?
Mentioned transformation has been made using JavaIO class from Java Image I/O API, which explains why xml files where initially excluded - they simply couldn't be parsed into valid bitmap image.

What has changed?

Understandably, enabling vector images support couldn’t be achieved just by including xml files. A good idea seemed to be evaluate existing resource overloading trick even further. In revised library version, when it detects a xml file containing Vector Drawable definition the procedure looks as follows:

  1. preserves original vector image under build variant specific res directory,
  2. generates overlay png images, using existing transformations
  3. composes them into single image resource using a LayerDrawable.
  4. saves composite image under original name, in build variant specific source set

That trick allows referencing original image in a file saved under the same name!

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

<item android:drawable="@drawable/
original_resource_copy" />

<item android:drawable="@drawable/
ribbon_overlay_1" />

<item android:drawable="@drawable/
ribbon_overlay_2" />

</layer-list>

Gradle

Other notable issue was upcoming incompatibility with Gradle 7.0 and painful lack of build cache support. The revised Gradle Plugin and Gradle Task definitions had to be almost completely reworked to leverage all recent Gradle features. Plugin code has been updated to benefit from Lazy Task Configuration and to configure tasks only when needed. In addition, plugin’s Gradle Task definition has been reworked to properly define its@Inputs and @Outputs which made it compatible with upcoming Gradle versions.
The most demanding part of the refactor was covering it with tests, as initially the plugin reported coverage at the level of 0.72%. At the moment it is at 56% leaving most of the untested code in the raw image transformation related code itself.

Continuous Integration

As mentioned above, old plugin suffered from poor environment setup. There were no pre-merge checks, there were almost no tests, there were no code style checks or even .editorconfig file committed to preserve the same file formatting across different contributors.
To address the issue, the goal was to have as much of the code tested and included as a part of Continuous Integration process.

Screenshot tests

Knowing that testing a UI specific code is a non-trivial task, screenshot tests were introduced, which compare actual launcher icon, with the preciously saved, expected one (for each user configuration). Allowing everyone to see how the plugin behaves right when a Pull Request is created.

Diff visible on each in Pull Requests. In this case when fixing icon overlay quality

Functional tests

Moreover, learning from my previous project when maintaining a Gradle Plugin it is highly profitable to have integrations tests that apply the plugin in a form that resembles the published one as much as possible.
There has been implemented a mechanism that publishes the plugin under unique coordinates in local maven repository as a CI pipeline step. Such a thing, allows to include a compiled artifact in a test project:

buildscript { 
repositories {
if (project.hasProperty()) { mavenLocal() }
}
dependencies {
if (project.hasProperty()) {
classpath "com.project.starter.local:easylauncher:+"
} else {
classpath "com.project.starter:easylauncher"
}
}
}

passing a -PuseMavenLocal argument to Gradle invocation makes a test project to use pre-compiled plugin binary.

Furthermore, using an already compiled, binary file, enables extremely easy testing with different Gradle versions! With a slight touch of Github Actions it is easily achievable to have a setup that runs a step using different build environments. Just by adding simple:

build-all-sample-apps: // <- job id
strategy:
matrix:
gradle: [ 6.1.1, current, rc ]

it is possible then to make sure the plugin works with the minimal supported, current, and upcoming Gradle version.

- uses: eskatos/gradle-command-action@v1
with:
build-root-directory: sample
gradle-version: ${{ matrix.gradle }}
dependencies-cache-enabled: true
arguments: assemble -PuseMavenLocal

see full workflow file for details.

The future

At the moment plugin has all issues reported in original repository fixed (or at least partially addressed). The plan is to support the plugin as long it is possible, but to prevent a similar thing as happened to its predecessors, repository ownership has been already moved to an organization account, hoping to find contributors willing to maintain it in the future.

I would love to hear some feedback so feel free to try it out and if you have any, please file an issue at: https://github.com/usefulness/easylauncher-gradle-plugin

--

--