In this post we want to share a bit about the complexity and challenges we had to overcome along with the technology and the process that have gone into building this new browser for Android.
Unlike iOS, where we found in Firefox a great starting point (a modern Swift codebase), for Android we chose a very different path. Rather than start from an existing browser, we wanted to build a new browser app from the ground up on top of the Chromium rendering engine.
For Android, we wanted to take advantage of the Kotlin language and Jetpack Compose as a system for building our app. These are new technologies and they aren’t widely in use by existing open source browsers.
When it comes to choosing the implementation of the web rendering engine, there are two common options, both building on top of Chromium. One option is to just fork all of Chrome and make your UI changes to it, the other is to build on top of the system WebView API. While the WebView API looks very enticing (maintained by Google and shipped to all devices), it is unfortunately not the best fit for building a complete browser.
That leaves forking Chrome or trying to slice up Chrome and just use the rendering engine parts if we want to build a complete browser on top of Chromium.
Chromium is super complex, and there are a lot of engineers making changes to it continuously. Releasing a new version every 4 weeks. Maintaining an invasive fork of a large open source project like Chromium is a large undertaking. But with the goal of imagining and building a new browser app on top of a modern stack with Kotlin and Jetpack Compose, what other option is there?
What if there was another way?
Ideally a cleanroom app built with Kotlin and Jetpack Compose using Android Studio and Gradle could consume the rendering engine from Chromium through an abstraction layer that would shield the browser app from the constant churn of Chromium and not be costly to maintain.
Our exploration led us to WebLayer–a “high level embedding API to support building a browser.” It provides an insulating API around the key parts of Chromium’s engine. However it remains unclear how well it’s supported within the Chromium community, but an API of this sort is what’s needed.
WebLayer alone is however not enough. It is intended to be bundled as part of the WebView as an alternative API to the guts of Chromium. We had to figure out if this API could be repurposed and bundled as part of the embedding application.
Unfortunately this undertaking was more complex than we had hoped. Using this API and the code underneath it as an Android library is not a ready-made option. It is designed to be bundled up as an APK and used by other apps through reflection. The Chromium code in question is a bunch of Java code, resources, native libraries and a very specific set of Android library dependencies.
This last bit is significant.
Any application UI code that wants to link against the Chromium code as a library would need to use the same set of dependencies, the same set of AndroidX libraries, etc. However, Chromium uses its own custom build system, not Gradle, to specify everything, and trying to align these dependencies would be a mess.
That sent us back to square one. We had to decide whether to just give up and build the application UI using Chromium’s build system instead of Gradle and accept that there is no low cost way of building on top of Chromium, or keep looking for some other way. Could we have our cake and eat it too?
Then it hit, what about Android feature modules? Could the Chromium code be packaged up as a feature module and deployed that way? The WebLayer code is designed to be loaded dynamically out of another package–as part of WebView–in a manner that isolates its dependencies.
Feature modules do provide a way to split up your application into multiple APKs and have those deployed to the device as a single application entry. If the application UI can be one of those APKs and the Chromium code can be a separate APK, then we’d be set.
Of course, the devils in the details of course, and it doesn’t just work out of the box. It turns out there are several knobs and switches that are key to pulling this off. First, the application needs to declare that it is using isolated splits, which is a barely documented feature of the system. This is specified as an attribute on the application manifest.
Second, the app bundle needs to be configured to treat the Chromium split as install-time and removable so that it will always be present on the device as a separate APK and not eligible to be merged / fused into the main APK. Note, this limits the solution to Android O or later.
Another crucial piece of the puzzle involves making sure resource IDs specified by Chromium and its dependencies can be resolved properly. This is accomplished via some remarkable hackery that is already present in the WebLayer implementation code but that needs some updating for our setup.
Finally and critically, it is necessary to use DelegateLastClassLoader instead of PathClassLoader when loading Java classes from the Chromium feature module. This way the dexPath of the feature module will be preferred over that of the base module, enabling the two modules to define the same symbols–from different versions of the same AndroidX libraries–and avoid runtime errors.
And with that overview, here’s the actual details in the form of a git commit that enables the WebLayer code and its dependencies to be neatly encapsulated into a feature module that a “normal” Android app can consume. (This technique can potentially be useful in other situations in which it would be nice to bundle up multiple distinct APKs into a single application and deploy them together.)
And with all of that, we now have a system that enables us to build a fresh browser app using modern technologies, standard Android build tools and is insulated from the constant churn of the Chromium project. This approach is how we built the Neeva browser for Android. We’ll have much more to say about how we used Kotlin and Jetpack Compose to bring the experience together.