· 5 min read Posted by Kevin Schildhorn
The challenge of adding a Web target to DroidconKotlin
In 2024 JetBrains reached out to see if we would consider adding web support to our DroidonKotlin repository. As one of Kotlin’s recommended multiplatform samples would be a valuable test ground and example for others to see how to add Web support.
It took a while for all the dependencies to be in place, but once support was available (especially SQLDelight support) we were finally able to add support for browsers. In this post we wanted to go over the difficulties and intricacies of adding web support to our project.
Lack of Dependency Support
JS vs WASM
One of the common hurdles of any newer target is making sure your dependencies support that target. We initially wanted to add WASM support to Droidcon, however not all of our dependencies supported WASM. Kotlin WASM is still in beta as of this writing so it’s not a huge surprise that it’s lacking support. What was more surprising is that some didn’t support web at all.
Waiting on SQLDelight
When we were first approached about this support, one of our main dependencies SQLDelight didn’t support Web. There weren’t any other libraries available for the database layer. So we waited to get started until SQLDelight added support (Room support for JS has since been added as well, but we’ve stuck with SQLDelight for now).
Flaky Web Support
Droidcon uses Coil for loading images from the web, which should be perfect as it supports Android, iOS and JS. However when implemented we were running into crashes involving tmpdir
Uncaught TypeError: tmp0_safe_receiver.tmpdir is not a function
It seems that when Coil initializes on JS, it creates an Okio FileSystem which is used for the disk cache. This file system is built for Node.js, but we’re targeting Browser, and Okio had no proper browser filesystem support. So even though you can call Coil from the browser it inherently assumes it has access to a server-like environment. One workaround was to use a specific JS implementation using a library called Kamel, which worked well.
On top of that Firebase Crashlytics does not have Web support. Luckily enough in this case we can just not log crashes on web, which isn’t ideal but also isn’t a deal breaker.
Localization and Resources
Android and iOS both have their own localization, however Web doesn’t really have a standardized localization library. Luckily Compose Multiplatform has its own Localization solution, which uses a system similar to Androids xml resource files. In our project this was mostly for notifications however this is still the best method for adding localization.
For resources we moved to Multiplatform Resources which worked well. The only thing to note is that you’ll need to add the Compose Plugin to your module.
Data Storage
Droidcon has multiple methods of data storage. There’s a database for stored information, as well as a resource reader for local json files.
Adaptive Layout
With Compose Multiplatform the UI will work on web out of the box, however the UI was designed for the mobile form factor. We wanted to make the UI in Web look cleaner without creating an entirely new UI, so we opted for Adaptive Layout. There were some complications around Navigation that we needed to fix, such as refreshing the list screen when a session was added or removed from the agenda, but for the most part it wasn’t as large of an endeavor as I was expecting.
Some screens needed an update as well, such as the Sponsor View. With mobile we were fitting a list of sponsor icons based on the width and keeping the ratio 1:1, but on web, the window is so wide it made the images giant.
Threading and SQLDelight Changes
For the database we were able to keep our SQLDelight database which was great, however there was one major change that needed to be made. You need to set generateAsync to true, meaning all the generated queries are called asynchronously. This is because on Web SQLDelight is using a Web Worker that runs in the background, and the main thread must talk to that worker. This might sound like not a big deal (just swap execute to await), but this means that now every place that you want to access the database has to be suspendable.
One large cascading issue we had involved our conference-driven app is our ConferenceConfigProvider class. When it is injected via koin, it first grabs the currently selected conference from the database and initiates the provider with that non-nullable Conference. In other words there is always a Conference in the app. With asynchronous calls now we cannot block the thread to pull in the conference during injection, so now all of its functions that rely on that conference are optional, and the classes that rely on those calls must handle optional requests as well, and so on. This also meant that any time any class needed to get the Conference’s Id or information it needed to be done in a suspend function.
Javascript Quirks
Many of our existing dependencies appeared to work initially but still required a few adjustments. For example, when we added Web support for SQLDelight we also needed to add sql.js to the project (SQLDelights sqljs-worker depends on it). That also needed to be configured with a config js file (under webpack.config.d/XYZ.js), removing node module references and adding the sql-wasm.wasm file which is needed for the worker.
The app also includes KotlinX DateTime support in various places. In Js the DateTime library relies on @js-joda/timezone, which needed to be added to the project. This meant we needed to initialize the timezone library by calling external object TimezoneInit and val tz = TimezoneInit.
Putting it all together
Those were our hurdles while adding Web support. There were some large changes, some hidden issues and more differences between mobile and web than we realized. It was a somewhat challenging task but most of the code was able to be shared using KMP and CMP.