DEV Community

Cover image for Open the default browser across platforms
Thomas Künneth
Thomas Künneth

Posted on

5

Open the default browser across platforms

Welcome to the third article in a series of tips and tricks about Compose Multiplatform. The content is based on a sample app called CMP Unit Converter. It runs on Android, iOS, and the Desktop. As its name suggests, you can convert between various units. While this may provide some value, the main goal is to show how to use Compose Multiplatform and a couple of other multiplatform libraries, focusing on platform integration. This time, we will be looking at opening the default browser across platforms.

CMP Unit Converter running on a foldable

So, why would you want to do that? Well, one obvious reason is that it's part of the app's purpose. For example, CMP Unit Converter shows some supporting information at the right hand side of the converter area. Which unit or scale was last selected? What's the essence of that unit or scale? To learn more, the user can get all insights about the unit or scale by clicking the Read more on Wikipedia button.

At this point, the app has two options:

  1. Show the web page inside the app
  2. Let the default browser do its job

Certainly, embedding the browser into the app offers a very cohesive experience. However, what happens if the user has more browsers installed? How do we handle navigation inside the browser? Are we sure the user wants to read the web page inside the app? A lot of apps try to solve this by adding a Show web pages inside the app toggle. But is that really necessary? Why not keeping things simple and just letting the app do its job that was made for showing web content?

Fire and forget

CMP Unit Converter does not interact with the web page. It relies on a simple fire and forget scenario. Let's define a simple expect function in commonMain:

expect fun openInBrowser(url: String, 
                         completionHandler: (Boolean) -> Unit = {})
Enter fullscreen mode Exit fullscreen mode

We are passing the url as a String because it is easily usable on all platforms. If needed, more specific objects can be created from it by platform-specific code. completionHandler is something I borrowed from iOS. So, let's look at the corresponding implementation.

actual fun openInBrowser(url: String,
                         completionHandler: (Boolean) -> Unit) {
  NSURL.URLWithString(url)?.let {
    UIApplication.sharedApplication.openURL(
      url = it, 
      options = emptyMap<Any?, Any>(),
      completionHandler = completionHandler
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

openURL() is an asynchronous operation; the function returns immediately. Once the asynchronous task is finished (either successfully or not), the completion handler will be invoked. options allows you to configure how the url will be opened. For a list of possible keys to include in this map, see UIApplication.OpenExternalURLOptionsKey

Having a completion handler that tells us whether opening the web page was successful certainly is more than fire and forget, but on the other hand we want to notify our users if something went wrong. So let's see how we achieve this on Android.

actual fun openInBrowser(url: String,
                         completionHandler: (Boolean) -> Unit) {
  val result = try {
    context.startActivity(
      Intent(
        Intent.ACTION_VIEW, Uri.parse(url)
      ).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
    true
  } catch (_: ActivityNotFoundException) {
    false
  }
  completionHandler(result)
}
Enter fullscreen mode Exit fullscreen mode

Since we don't know which app will be reacting upon Intent.ACTION_VIEW, we need to use startActivity() which really means fire and forget. The only thing we can and should do is catch ActivityNotFoundException.

Finally, let's look at the Desktop.

actual fun openInBrowser(url: String,
                         completionHandler: (Boolean) -> Unit) =
  browse(url = url, completionHandler = completionHandler)
Enter fullscreen mode Exit fullscreen mode

I maintain a file called DesktopHelpers.kt in desktopMain which heavily uses java.awt.Desktop. Sadly, this class has never been particularly well-known. It was first introduced all the way back in Java Platform, Standard Edition 6, and received major additions in Java 9.

fun browse(url: String, completionHandler: (Boolean) -> Unit = {}) {
  with(Desktop.getDesktop()) {
    val result = if (isSupported(Desktop.Action.BROWSE)) {
      try {
        browse(URI.create(url))
        true
      } catch (e: IOException) {
        false
      } catch (e: SecurityException) {
        false
      }
    } else false
    completionHandler(result)
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Desktop features always consists of these steps:

  • Get a Desktop instance by using Desktop.getDesktop()
  • Check if the required function is available using isSupported()
  • Invoke the function

So, the code snippet above invokes completionHandler() with a true value if Desktop.Action.BROWSE is a supported action and browse(URI.create(url)) does not throw an exception.

Wrap-up

Besides Desktop.Action.BROWSE there are a few other actions available. For example, you can invoke an email client and set Preferences, as well as, About handlers. I might return to this in future parts of this series.

Quadratic AI

Quadratic AI – The Spreadsheet with AI, Code, and Connections

  • AI-Powered Insights: Ask questions in plain English and get instant visualizations
  • Multi-Language Support: Seamlessly switch between Python, SQL, and JavaScript in one workspace
  • Zero Setup Required: Connect to databases or drag-and-drop files straight from your browser
  • Live Collaboration: Work together in real-time, no matter where your team is located
  • Beyond Formulas: Tackle complex analysis that traditional spreadsheets can't handle

Get started for free.

Watch The Demo 📊✨

Top comments (3)

Collapse
 
skaldebane profile image
Houssam Elbadissi • Edited

Compose seems to have a built-in UriHandler, which you can get using LocalUriHandler.current.

It has a single function, fun openUri(uri: String), which throws IllegalArgumentException if the uri is invalid or the system can't handle it.

Unsure if this was only added (or made public) recently, but I'd guess it's there to handle link annotations in text.

Collapse
 
luca_nicoletti profile image
Luca Nicoletti

Where does the 'context' in the Android implementation come from?

Collapse
 
tkuenneth profile image
Thomas Künneth

It's injected using Koin in Platform.android.kt:

private val context: Context by inject(Context::class.java)
Enter fullscreen mode Exit fullscreen mode

Jetbrains Survey

Calling all developers!

Participate in the Developer Ecosystem Survey 2025 and get the chance to win a MacBook Pro, an iPhone 16, or other exciting prizes. Contribute to our research on the development landscape.

Take the survey