Web-based drag-and-drop in Compose Multiplatform

Manel Martos Roldán
7 min readJun 4, 2024

--

Photo credit: Robert Hoge

I’ve recently published a project called LottieViewer, built with Compose Multiplatform, for rendering Lottie files. The initial implementation was a desktop application where you can easily drag and drop Lottie file animations to view them. I have iterated on the project, making it available for Web by adding support for Kotlin/Wasm. It works in the same way but new uses Web APIs. Let’s explore how I achieved this new milestone.

Add Kotlin/Wasm target

When creating a new Compose Multiplatform project, the preferred choice is to use JetBrains’ Wizard and select all applicable platforms. If, at any time after setup, you find that an additional platform needs to be added, you will need to do it manually. That’s what I did with LottieViewer. It isn’t rocket science, but let’s see how to add Kotlin/Wasm to an existing project.

First, we need to declare the new target in the composeApp/build.gradle file by adding a few lines:

Secondly, we need to declare the main function along with the invocation to the App composable. The file we should add is called main.kt and should be added to the composeApp/src/wasmJsMain/kotlin folder with this content:

Finally, we need to add some basic HTML source code to wrap the App. In this case, we just have to add the index.html file to the composeApp/src/wasmJsMain/resources folder with this content:

JetBrains ViewModels and Lifecycle Libraries

The previous version of LottieViewer used Google’s libraries for ViewModels and Lifecycle handling for desktop. It worked nicely because those libraries support Android, iOS and JVM, but since we’re adding Web as a new target, we cannot use them anymore. Fortunately, JetBrains already has a fork from those libraries which adds the support we need. By updating the proper dependencies in libs.version.toml file, we should end up having something like this:

Understanding how drag-and-drop works on Web

This is a common operation with well-defined APIs and guidelines. I followed the instructions described on the MDN page for File drag and drop. Here is what is needed:

  • Define the drop zone.
  • Add handlers for drag-and-drop events.

The following sections will describe all the necessary Web code for accepting file drops.

Define the drop zone

As described in the MDN guide, this involves adding three new attributes to the HTML element that will handle drag-and-drop events: ondrop, ondragover, and ondragleave. In our case, the best candidate for this is the canvas element, as it hosts our application. This is how it should look:

There is a downside to the approach we’re following: the entire application hosted in the canvas will react to drag-and-drop events. This is not a big deal for our use case, but if you want a smaller drop area, you could potentially work with the coordinates received by drag-and-drop events (attributes screenX and screenY) and map them to the appropriate Compose element that should react to them appropiatelly.

Drag-and-Drop Event Handlers

Now it’s time to define the necessary JavaScript code to handle Web-based drag-and-drop operations properly. We referenced these handlers in the previous section and will now reveal their implementation details.

First, we should provide some interop functions between JavaScript and Kotlin. This way, when a user picks a file and starts the drag-and-drop operation, we can notify the Kotlin code from the JavaScript code. With that in mind, I defined a DragAndDropListener object that we can easily register from Kotlin and invoke from JavaScript when needed. Here is the relevant code snippet:

Later on, we’ll see the context in which register/unregister functions are invoked.

Now it’s time to see what the handlers look like:

As you can see, all of them follow the same structure: first, they call the preventDefault function from the event. This is necessary because the default implementation is to open the dropped file, which is not what we need. After that, the equivalent method from our listener is invoked. There is an exception with dropHandler, which calls the extractSingleFile function to retrieve the file object that has been dropped. The details of extractSingleFile aren’t shared here as they aren’t particularly interesting.

All this code has been added to a file named drag.js and included in index.html as shown below:

Back to the Compose World

In the earlier version of LottieViewer, I was using the onExternalDrag modifier, defined only for desktop (link here), to handle drag-and-drop operations. However, this solution doesn’t scale when adding a Web target, as this modifier is undefined for Web.

So, we need to generalize the drag-and-drop functionality in a way that can be implemented differently based on the target being compiled. The good news is that we already have a solution: expect/actual declarations and we can also define modifiers and composables using this approach:

This is a straightforward modifier that closely resembles the JavaScript DragAndDropListener we’ve already defined. An important detail here is the onSingleFileDropped function, which accepts a single parameter of type FileDesc. This is a convenient way to generalize the implementation details of how files are referenced based on the underlying platform. We used a single URI for desktop and a JavaScript File instance for the Web. It would be great if we could use generics here, but they are not supported yet.

Implementing onDragAndDrop for Kotlin/Wasm

Let’s deep dive into how to implement the onDragAndDrop modifier in Kotlin/Wasm. Here, we want to call some predefined JavaScript code to bring drag-and-drop functionality from Web into Compose. We can achieve this by importing the defined JS functions into Kotlin using the external keyword:

Now, let’s look at the onDragAndDrop implementation:

We use the composed API to define this new modifier. Additionally, by leveraging DisposableEffect, we can register the incoming Kotlin methods with their JS counterparts. And of course, to ensure proper cleanup, we call the unregister function inside the onDispose block. In the onDrop callback, we wrap the JS File instance into a JsFileDesc type that extends from FileDesc interface and is available only in the scope of Kotlin/Wasm.

That’s it! This is all we need to get Web-based drag-and-drop mechanisms working seamlessly in a Kotlin Multiplatform project. But remember, file handling is a common requirement, so we’ll need to address that next.

File handling

Reading the content of a dropped file is also platform-specific. We can again leverage expect/actual patterns to handle this. I already have a neat functional interface called FileStore used by the underlying ViewModel for reading, parsing files, and displaying Lottie animations. Now, when adding Web as a new target, we need to revisit and adapt it slightly:

The createFileStore function lets us instantiate a platform-specific implementation of the FileStore functional interface. Here’s the Web-based implementation:

Here, we check if the incoming fileDesc is a JsFileDesc and throw an exception if not. We then use suspendCoroutine from the Coroutines API to handle the asynchronous nature of FileReader, which is defined org.w3c.files package and exposes the corresponding JavaScript functionality (check its API here). Inside the block, we provide a lambda for the onload callback that resumes the coroutine with the file content. Finally, we call readAsText on the FileReader. The resulting value from suspendCoroutine is then emitted by the underlying flow.

Recap

Thanks to JS/Kotlin interoperability, we’ve created a new Web app that supports drag-and-drop of any file from your computer. Here’s what we’ve achieved:

  • Defined a drop zone in HTML code.
  • Exposed a fully functional JS listener for drag-and-drop.
  • Integrated drag-and-drop functionality in Compose using a simple modifier.
  • Contextualized file handling for a multiplatform project.

But you may be wondering: why the hell add Web as a target? The answer is simple: to deploy with GitHub Actions and make it available to everyone!

Extra-ball: deploy with GitHub Actions

It’s not a secret that GitHub Actions are incredibly powerful. They allow you to trigger specific tasks at any point in the SDLC (Software Development Lifecycle) effortlessly, making them a dream tool for automating tasks. That’s exactly what we want to do: automatically compile and build the Web version of LottieViewer and deploy it to GitHub Pages.

Let’s break down the workflow:

Every GitHub Action needs a name and an event to trigger it. In this case, we want it to run whenever there’s a push to the main branch.

This section defines a job named Deploy and the operating system (Ubuntu) it will run on. Now, let’s look at the deployment steps:

This step checks out the code from the repository using a personal access token (PAT) for secure checkout (more details at GitHub Docs).

These steps ensure everything is set up to build any project with Gradle. The last step runs a task to compile and get all the files needed for the Web app which we’ll deploy later.

Finally, some git commands. We create a new orphan branch (starting fresh with no prior commits), add all the built files, and then commit and push them. Boom! The Web version of LottieViewer is ready to play any Lottie file on your desktop!

Edit: Safari compatibility issues with Kotlin/wasm

There is an issue when trying to run code targeting Kotlin/Wasm on the Safari browser. That’s why I would recommend relying on Kotlin/JS in the meantime. This involves using a different Gradle task (:compose:jsBrowserDistribution) and copying the produced artifacts from a different directory (composeApp/build/dist/js/productionExecutable).

Conclusion

With a few tricks, it’s amazing how much you can achieve with Kotlin Multiplatform projects. LottieViewer is just an example of how you can combine different technologies to create something new and useful.

Check out the whole project on GitHub: https://github.com/manuel-martos/LottieViewer

And feel free to use it on this GitHub Page: https://manuel-martos.github.io/LottieViewer

Thanks for reading to the end!

--

--

Manel Martos Roldán

Software engineer passionate about creative coding and mobile development