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. 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: Show the web page inside the app 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 = {}) 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(), completionHandler = completionHandler ) } } 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) } 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) 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) } } 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 part

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.
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:
- Show the web page inside the app
- 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 = {})
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
)
}
}
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)
}
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)
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)
}
}
Using Desktop features always consists of these steps:
- Get a
Desktop
instance by usingDesktop.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.