Monday, May 25, 2015

What To Expect When You're Expecting...

(...To Move Your App to Android Studio)


Introduction

I recently migrated a decently sized Android project that was originally built in Eclipse with ADT into Android Studio. I've been developing in Java for nearly 14yrs now and Eclipse has been my weapon of choice for ~10 of those years (just... don't ask about the other four). Inasmuch, all the tinkering and whatnot I've done in Android for the past 4-5yrs has been in Eclipse, too. Between all the buzz about Android Studio and the irrefutable, stone cold fact that all the cool kids use IntelliJ, I had been looking forward to changing IDEs for some time to see how much greener the grass is on the other side. As fate would have it, business-related happenings at my current place of employment made the new "build flavors" configuration option (by way of Studio's Gradle plug-in) a veritable must-have, so thus began my journey to the other side.

While I'm satisfied with the variant-based solution we eventually arrived at in switching over to Studio, there were some unexpected hurdles to overcome during the transition in order to begin implementing that solution. The following is an account of those hurdles and what I did to get over them.

Some information about my setup:
  • Android Studio 1.1.0 (though I think this write-up still holds for the 1.2.x branch)
  • Gradle Plug-in 1.1.2
  • Robolectric 3.0-rc2
  • OS X Yosemite
  • The migration itself was performed using Studio's "Import Project" wizard
  • Pre-Lollipop build

MultiDex

As you well know, your source code morphs a few times before it's actually ready to be run on an Android enabled device. Assuming Java as your language of choice, that lifecycle looks a little something like this:

  1. Firstly, your .java files are compiled into your normal VM-spec compliant .class files. 
  2. Since Android devices run a different variant of VM, named Dalvik (the default runtime for pre-Lollipop devices), the aforementioned .class files are then converted into Dalvik EXecutable files (.dex)
  3. Lastly, those .dex files (along with your compiled manifest, resources, etc) are packaged into an installable package (.apk) file.
Through this process, those of you with large projects or perhaps projects that have runtime dependencies on large third-party libraries, may have frustratingly come to learn that the Dalvik specification limits the number of methods that can be referenced within a single .dex file to 65,536.  (Why? <shrug>).  Should you breach this limit you'll be met with an aptly descriptive build error that looks like something along these lines:

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

What was really odd in my case was that before the transition to Studio I wasn't hitting this limit. It was only after the migration that my app wouldn't build as a result of hitting this 65k method limit. My (lazy schmuck) theory as to why this happened was that the import wizard may have converted some of the .jar files in my libs/ folder into Gradle managed dependencies, and in that process they maybe got swapped for later versions of those same libraries... which were larger? I know, I know. At the time I just wanted to get things working again, so rather than investing time in figuring out what changed I went about resolving the problem the build system was reporting.

While the recommended solution to this problem is to reduce the number of method references in your application, sometimes this just isn't possible (such as in the case of third-party libraries). Therefore Google's Android Team has produced a solution wherein your build's supplementary .dex files are referenced by the support library (for pre-Lollipop builds), or just natively supports handling multiple .dex files (for builds done against Lollipop or later).

In my case (pre-Lollipop with a custom android.os.Application class), enabling multidex handling involved two steps:

1) Updating the app's build.gradle file to set the multiDexEnabled flag, and importing the multidex support library as a dependency:

android {
  defaultConfig {
    multiDexEnabled = true
  }
}
dependencies {
  compile 'com.android.support:multidex:1.0.0'
}

2) Overriding Application.attachBaseContext() in my custom Application class:

@Override 
protected void attachBaseContext(Context base) { 
    super.attachBaseContext(base); 
    MultiDex.install(this); 
}

The extra emphasis on there being two steps comes from a mistake I made early on. I initially didn't realize the change to my Application class needed to be done. This resulted in the app building, but constantly crashing at runtime due to ClassNotFoundExceptions. Oblivious to what was going on, I unarchived my .apk file and noticed a few .dex files:
  • classes.dex
  • classes1.dex
  • ...
Strangely enough, it was within classes1.dex that the classes identified in the ClassNotFoundExceptions were hiding. Tugging on this thread led me on a few Google searches that helped bring me to the realization of the mistake I had made, which, hopefully now you won't.


Robolectric

...is now a first class citizen in the development cycle (as of Studio 1.2.x)! Rejoice ye responsible developer!

That's not to say we're completely problem free though.


Test Runner Configuration

Robolectric doesn't seem to resolve the application ID correctly when testing against different build flavors. However, creating a dummy BuildConfig with a hard-coded application ID (reflective of the main source tree) seems to be enough to get by in lieu of a real fix. 

My hacky little custom BuildConfig looks something like this:

public class TestBuildConfig {
    public static final boolean DEBUG = BuildConfig.DEBUG;
    public static final String APPLICATION_ID = "your.main.package.name";
    public static final String BUILD_TYPE = BuildConfig.BUILD_TYPE;
    public static final String FLAVOR = BuildConfig.FLAVOR;
    public static final int VERSION_CODE = BuildConfig.VERSION_CODE;
    public static final String VERSION_NAME = BuildConfig.VERSION_NAME;
}

Just refer to this class in your test's configuration annotation, like so:

@Config(constants=TestBuildConfig.class)

...among whatever other configuration options you intend to pass along.

To be fair, I'm not sure if this is a byproduct of my using a release candidate of Robolectric... although in my defense this seems to be a rather glaring problem for a second release candidate. But hey, we're all friends here.


Test Directories

If you're using build flavors in your setup and you're wondering what the directory structure for flavor-specific tests is supposed to look like, here it is:

  -- src
    |-- flavor1
    |-- flavor2
    |-- main
    |-- test
    |-- testFlavor1
    `-- testFlavor2

assertj + assertj-android

If you're using assertj, don't bother trying to use the 2.x branch or later for testing your Android code. It uses a Path-based approach to making assertions, which isn't (yet?) compatible with Dalvik.

Stick with the latest release on the 1.x branch.

Dagger + ButterKnife

These amazingly work right out of the box with little-to-no configuration changes.

The one hiccup I experienced had to do with Studio's overzealous import wizard actually importing the generated code from these frameworks, thus resulting in subsequent builds failing due to classes already being defined when the frameworks attempt to re-generate them. The build system is likely to report an error along these lines:

Error:(7, 8) error: duplicate class: com.android.test.Foo$$ViewInjector


Annoying, yes, but thankfully simple to resolve. Traverse your source tree and delete all the classes with names that are suffixed with:
  • $$InjectAdapter
  • $$ModuleAdapter
  • $$ViewInjector