Android Instant Apps, step-by-step: how Vimeo went about it
What are Android Instant Apps?
As an Android user, I’m ecstatic to say that Google has finally made Android Instant Apps (AIA) public to developers. AIA is a new feature built into the Android operating system (available on everything back to Lollipop), allowing users to open individual features of your app without even having it installed. At Vimeo, we currently have an AIA with just one feature — our video player. This means that if someone clicks a Vimeo link, it’ll bring them to a nearly-identical playback experience as to what they’d enjoy on our full native mobile app — even if they don’t have the Vimeo app installed.
Vimeo was one of a handful of companies with early access to AIA, and we worked with Google to get our video player Instant App released by Google I/O. In the process of getting a 15MB full app down to a 4MB AIA feature, we learned a lot along the way. In this post I’ll try to cover some of the lessons we picked up around refactoring a large-production app, shrinking APK size, and some additional UX UI improvements we were able to make on our Instant App.
The Android developer documentation does a great job of covering all the basics of AIA, so start there or with this great post if you’re not familiar with setting up a project or roughly how the whole system works.
How do I refactor my massive app to support AIA features?
Say your current app is two years old, >15MB, has eight large features (and a load of dependencies), *and* you want to support at least one feature as an Instant App — how do you do it?
We ran into this situation back in February. We worked it out by developing a two-phase plan to get our player Instant App out as soon as possible (to meet the I/O deadline), while simultaneously paving the road for future AIA features beyond video playback. Hear about our first pass at AIA in Phase One — or feel free to jump to Phase Two where I talk about the right way to approach AIA implementation (which I also cover in the AIA panel at Google I/O).
Phase one: the quick and dirty approach
Now, as a warning, this first step is just about the opposite of what Google recommends for their Instant Apps — and I also highly advise against it for reasons I’ll discuss. But with only enough resources to put one engineer on the project at a time, we had to get creative with how we’d get our AIA feature finished in time. So right after joining the early access program with Google, we outlined the steps we’d take in our initial phase:
- Delete everything that isn’t player code
- Trim down the AIA to 4MB
- Fix the bugs we created in steps 1 and 2
- Modify UI to adhere to AIA UX best practices
Delete everything. We branched off of the “Vimeo Android” codebase and started deleting every Java class that wasn’t used by our player code, directly or indirectly. This amounted to a great deal of trial and error, but left us with a simple app that could launch our Vimeo player at a fraction of the full app’s size. Once we deleted all the Java classes, we used Android Studio’s nifty “Refactor — >Remove Unused Resources”, which cleared out all the layouts and drawables that were no longer referenced. This entire process only took about a day and got us from 15MB down to 8MB.
Trim down. Now, we only had about 4MB to go — but I assure you the last few MBs are the hardest. By using the APK Analyzer built into Android Studio, we found that a large portion of those 8MBs were coming from our larger third party dependencies.
Within the lib folder (left), you’ll see the different .so files which target individual ABIs. Currently every .so file (for every ABI) will be compiled into your feature APK — though eventually, Instant Apps may support splitting APKs based on different Android architectures. We found that libpano_video_renderer.so was coming from our dependency on the Cardboard VR SDK because it’s a library that uses the NDK, and libimagepipeline.so was coming from Fresco, our image caching library. Between the classes and .so files, Fresco was around 2MB and Cardboard was around 2.5MB.
Since we were working in a branch off of our actual app, we just deleted every reference to the Cardboard SDK. Since it was decoupled from our code, it was very easy to remove. Fresco, on the other hand, was referenced all over (and even in XML), which made it much more difficult. We ended up swapping out all of references to Fresco code with Picasso, a much smaller image caching library (<200KB).
This step turned out to be the most enlightening: it helped pinpoint exactly which large dependencies needed to be fixed and it influenced our strategy going forward, which I’ll outline in our Phase Two.
Fix the bugs. We saw some issues arise from the previous steps — mostly around buttons not working anymore due to functionality being removed, such as the cardboard button. But we also saw some nuanced changes, such as the loss of support for rounding the user’s avatar image since that was something we got for free from Fresco.
Instant App-ify the UI. We followed the Android UX guidelines pretty closely, but the short version is this: make it look like your full app, no splash screens, 2–3 implicit install prompts, and at least one explicit install prompt. For the implicit install prompts, we show explanatory dialogs if a person tries to Chromecast or comment on a video (if they’re not logged in). Explicit install prompts, on the other hand, must have the “install” icon, but don’t need to show an explanatory dialog. We only have one explicit install prompt button which mentions that the user can install the app to watch videos offline. We chose this text because it refers to a feature that isn’t available in an instant app, and is a popular feature in our full app. However, other than those few additional dialogs and the install button, the UI is identical to our full application.
Phase two: the right approach
As mentioned above, I don’t recommend the Phase One approach since you’ll end up with two code bases you have to maintain. But, if you absolutely need to get an AIA feature out quickly, then it will likely be your fastest option. If you have the time, however, I recommend untangling your dependencies one feature at a time. This post from a developer at Jet outlines many of the same strategies we’ve used for modularizing our app. I’ll try to briefly cover the pieces we found to be most important that allow you to incrementally get closer to your first AIA feature.
- Get an idea of your larger dependencies
- Remove, replace, or abstract the dependencies you can
- Rely heavily on composition and dependency injection (DI)
Use tools to get an idea of your large dependencies. The APK Analyzer is a great place to start. In addition to seeing .so files, you can see which libraries have the most methods by clicking on classes.dex. Below on the left you can see com.google.android accounting for a large number of methods. We then were able to use a tool like the Dexcount Gradle Plugin, which gave a nice graphical view of our APK. From these two, we realized we could save a good chunk of space (and remove any need for play-services) by removing Chromecast functionality, which wasn’t even supported by AIA at the time.
To see a visualization of the tree of dependencies (maybe a library is defined as a dependency of several of your other dependencies), you can use the below command with your own app module name.
./gradlew -q dependencies <module_name>:dependencies --configuration compile
Note: If you’re using v3.0.0 of the Gradle plugin, you can use implementation instead of compile — or leave the configuration parameter off entirely (-q just hides log messages).
Another way to get a rough idea of the size of some of these dependencies is by entering their package name directly into Methods Count, which will include the size (in KB) and the libraries they depend on.
The last tools I’ll mention are strictly about reducing size with little effort: ProGuard and ReDex, both of which are bytecode (dex) optimizers. ProGuard you’ll get with Android, and ReDex is an open-source library by Facebook.
Removing, replacing, and abstracting dependencies.
So now that you’ve isolated all of your large dependencies, how do you “take care of them”? That answer definitely varies case by case.
Removing. If you see that you’re depending on a library for a small subset of the functionality it offers, it may be better to remove that dependency altogether and write your own implementation. A classic example of this is importing the entire library of Guava for just a few string utilities. ProGuard will help remove unused code, but it won’t stop people from using other parts of the library. If at all possible, rely on very targeted libraries or turn to your own implementation for simple functionality.
Replacing or abstracting. In the case of image caching, we need a solution that will work for just about every conceivable AIA feature of our app, which means we can’t just remove caching. Additionally, we want to use Fresco for the full app (because it has lower level optimizations), but a smaller library for all of our Instant App features. So the solution we’ve chosen is to swap all calls to Fresco out with an interface that mirrors the API that we need. That way the full app can use Fresco, and the Instant App can use a smaller library, like Picasso or our own image caching implementation.
Rely heavily on composition and dependency injection (DI). We learned DI was crucial during our phase of removing all non-player code. There were multiple sets of functionality related to our player that weren’t required for the instant app, such as the playback of encrypted/downloaded files (since there’s no storage in AIA), Chromecasting, and 360 video and VR playback. So we took a page out of ExoPlayer’s book and relied on composition to inject functionality into our core player. We made it so the core player “presentation” and UI layers of our player architecture didn’t care about how videos were played. Instead, you could inject different “PlayerEngines” into the core player that would describe the functionality the player should use. With this architecture, we could easily omit the engines not necessary for AIA which also meant we could omit the required dependencies (Chromecast SDK, Cardboard SDK, encryption code).
Another benefit of using DI is that if you wrap all core functionality in interfaces, you can make the interfaces optional so that your system can live without it. If you were to wrap your crash reporting implementation in a nullable interface, you could choose to omit it for your AIA and rely on the developer console’s crash reporting, but include a different implementation in your full application. The same could be said for any singletons you may use in your code: we have an AuthSingleton, which is referenced heavily to see if a user is logged in. However, there may be AIA features which don’t need to know about auth. If it were wrapped in a nullable interface and injected everywhere it was needed, then it could be easily removed.
Putting it all together
Rome wasn’t built in a day, and the Romans certainly would have taken time to build Instant Apps with the TLC they deserve if they had the right tech. If there’s no rush to get an Instant App feature out, take your time to do it the right way. Using some techniques outlined above, you can incrementally get your APK size down and untangle your dependencies. Start with one large dependency at a time, remove your reliance on those libraries directly, and replace them with interfaces. Once all your code relies more on composition/DI, you can start breaking it into the different library modules and untangle your dependency tree even further. Lastly, for all new features going forward you should build them in their own module. This makes it harder to add unnecessary dependencies and forces you to keep your features lean. If that new feature will rely on some functionality in the app, pull that functionality into it’s own module. Oh, and keep a close eye on all your build.gradle files. Good luck!
Interested in flexing your engineering chops at Vimeo? Join our team!