Wyko Rijnsburger     About     Feed

Comparing Build Performance between JDKs in Android Studio

By default Android Studio builds projects using its embedded JDK8. IntelliJ IDEA product manager Dmitry Jemerov says you should use the embedded JDK in Android Studio. My typical workflow while developing is:

  • Write code in Android Studio.
  • Verify the result by either running the app or running a specific set of tests in Android Studio.
  • If everything is OK, run all tests and the linter through the terminal.

In the terminal I use the JDK set as JAVA_HOME by jenv. I want my JDK setup decoupled from Android Studio, using the embedded JDK as the default Java is not an option.

Using two different JDKs creates two separate Gradle Daemons. The Daemon contains the build cache, so one JDK cannot use the build cache of the other JDK. In this case, running all tests and the linter cannot reuse the build cache from the embedded JDK so this step takes a lot longer than necessary.

Using the same JDK in both Android Studio and the terminal solves this, although this goes against Dmitry’s recommendation. The project I work on is compatible with JDK14, so for a few weeks I used JDK14 in both places. I had the feeling that builds run in Android Studio were taking longer than with the embedded JDK, so I ran a quick, non conclusive experiment comparing the build performance of Android Studio (see test setup below).

My simple test does not show any significant difference between JDKs. So until there is a good reason to switch back to the embedded JDK, I will continue using JDK14 in Android Studio.

  • Embedded JDK8 : 181.1
  • AdoptOpenJDK 1.8.0_265 : 170.6
  • AdoptOpenJDK 14.0.2 : 185.2

(Values are in seconds.)

Test Setup

I rebuilt the project from Android Studio using each JDK six times. I excluded the results of the first rebuild after switching JDKs as rebuilding reuses some of the previous builds config.

Project

  • 37 gradle modules
  • KAPT with Dagger
  • 90% Kotlin, 10% Java
  • Jetifier enabled

Machine

  • MacBook Pro (15-inch 2018)
  • Processor: 2,5 GHz 6-core Intel Core i7
  • Memory: 16GB 240MHz DDR4
  • Graphics: Intel UHD Graphics 650 1536 MB
  • OS: macOS Catalina 10.15.6

Android Studio

Version 4.0.1. Custom VM options:

-Xms1G
-Xmx2048m
-XX:MetaspaceSize=512m

Introducing Kotlin Multiplatform incrementally

You can use Kotlin for cross-platform development using Kotlin Multiplatform. Its approach differs from other cross-platform solutions such as Flutter and React Native: you can add it to existing codebases and it does not replace all platform-native code.

At Blendle, we wanted to experiment with Kotlin Multiplatform on the Android, Web and iOS clients. Using a tool to build an actual production feature is usually the quickest way to evaluate a tool, so we tried to figure out the easiest and fastest way to do this. The setup also needed to be easily removeable. This was especially important for the web/iOS developers. Merging Multiplatform code back into the Android app is trivial as the majority of the Android codebase was already written in Kotlin, but this is not the case for the other platforms.

We evaluated several approaches. I recommend checking out this talk by Ben Asher and Alec Strong for an in depth evaluation of these approaches:

  • Merge Android/Web/iOS codebases into a single Git repository (a mono-repo), add multiplatform code there.
  • Duplicate the Multiplatform code in each client codebase.
  • Create a separate repo for Multiplatform code.

We created a separate repo with its own CI pipeline. This isolated all Multiplatform code, apart from the integration into the clients, into a removable component.

The first feature we build in Multiplatform was email verification: when a user signs-up with a suspicious email address (e.g. user@gnail.com) we suggest a corrected email address before the user confirms their registration. A simplified version of this already worked: we fetched a verification regex from the backend, but we were not satisfied with that implementation. Email verification was a perfect use-case for Multiplatform because:

  • The logic should be identical for each client.
  • The email address should be verified after each key stroke. Doing the verification server-side requires a lot of traffic and adds latency.
  • It is easily testable in isolation.

After writing the Multiplatform code, we integrated the library into the Android, Web and iOS clients.

On Android, we assembled a library JAR by running the gradle assemble task. The Android repository included the output as a local library. This is suboptimal as releasing new versions requires quite some manual work and it increases the size of the Android git repo. However, we could start integrating the Multiplatform code within a few minutes.

On iOS we started out with a similar approach by producing a .framework file and including that in the iOS repository. This did not work as iOS requires different frameworks for different processor architectures. So we added a Gradle task producing a Fat Framework supporting both architecture (see this post for details). We did not want to use the Fat Framework for release builds though as it increases the size of the binary, so we created a BuildPhase in the iOS build that chooses the correct framework based on the build configuration (FAT for debug or the proper processor architecture framework for release builds).

On Web, we could not get the setup to work. The functions in the commonMain module were not properly exported to the Javascript outputs and the output file size was too large to include for such a simple feature. Since there are so many changes to targeting Javascript with Multiplatform in the upcoming Kotlin 1.4 we decided to postpone the integration with Web until after the Kotlin 1.4 release.

Apart from Web, we were quite happy with the resulting setup. Using the Multiplatform Email Verification API from iOS and Android was easy and we were even able to use a Sealed Class as a result type in the API. Since the first iteration we’ve build several more features in the Multiplatform library, with a majority being build in Kotlin by iOS developers! We want to make improvements to the setup, such as:

  • Pushing the JVM binaries to an artifactory. We do not use a custom artifactory at Blendle but GitHub actions (which we already use) offers an easy way to publish packages.
  • Including the Multiplatform repository into the Android build process for debug builds so that we can test changes to the Multiplatform code without having to produce a JAR.

KMinRandom 1.0.0

Today, I published the 1.0.0 version of KMinRandom, a library for generating random test data in Kotlin. I’ve been using the library for the past two years at both Bol.com and Blendle without any major issues, so it seems appropriate to publish a “real” version. You can check out the documentation and source code at GitHub. Any comments, feature requests or ideas are very much welcome!

Lessons Learned from implementing a Fragment to Fragment Shared Element transition

Discover transition gif Recent issues transition gif

1. Fragments should be in the same container

You cannot do shared element transitions with fragments in another container.

2. Use .replace() instead of .add()

Shared element transitions do not work when adding fragments, even to the same container.

3. Enable reordering allowed when postponing transitions

When the content of the transition needs to be loaded before the transition can start, we need to use postponeEnterTransition(). Postponing the transition will only work when you add setReorderingAllowed(true) to your fragment transition (see Android docs and the Reordering part of this blog post by Chris Banes for more context.)

4. Properly resume the state of the fragment you return to with the return transition

In this case, the view that needs to be resumed contains a nested RecyclerView within the main RecyclerView. The shared element needs to be in view in both fragments when the transition takes place. Therefore the nested RecyclerView needs to be restored to it’s previous state before the transition is started. This is done automatically when we use .add() to add the view because that means the view will remain in memory. However, using .replace() means that the fragment to which we need to return has been destroyed and needs to be recreated through the fragment lifecycle.

Using the onSavedInstanceState() and onRestoreInstanceState() methods of the nested RecyclerView, the state needs to be manually restored. startPostponedEnterTransition should be called when the shared element on the restored fragment is fully rendered.

5. To transition a CardView with an image, add both the card and the image as shared elements

Doing the transition on just the CardView won’t dynamically scale the image during the transition. If you do the transition on just the image, the card already be at its final position at the start of the transition.

6. postponeEnterTransition() and startPostponedEnterTransition() also work for return transitions

The naming of these functions is really confusing but they work exactly the same for return transitions.

7. setCustomAnimations fade-in/out causes shared element flicker. Individual fade-in/out fixes this

Using setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) to add the fragment fade-in/out animation causes a subtle flicker on the shared element whenever the shared element transition is finished. This is fixed by setting a separate fade-in/out transition on the individual fragments:

currentFragment.exitTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.fade)
newFragment.enterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.fade)

However, these transitions persist past this fragment transaction. If the exitTransition of the fragment is not reset, a fade will be shown when returning to this fragment from other fragments, even if no specific animation is specified. We can easily clear this by resetting the exitTransition when the shared element transition is finished.

fragment.sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move).addListener(object : TransitionListenerAdapter() {
    override fun onTransitionEnd(transition: Transition) {
        // The current fragment transition should only be applied for this transition and be removed afterwards
        currentFragment.exitTransition = null
    }
})

The Reactive Rollercoaster

I recently gave a talk at the Bol.com conference ‘Spaces Summit’ on how we used Reactive technologies to build a new backend for the Bol.com mobile apps. In the talk, I discuss the pros and cons of a Reactive approach, tell some horror stories about the issues we ran into during the development and I give tips and tricks for building resilient and performant Reactive JVM backend apps. The slides can be found in the description of the YouTube video.