D
Domen Laniลกnik
Guest
Building a Space Flight News App with Compose Multiplatform for Android, iOS, and Desktop: Part 5โโโNative Interactions
This is the fifth and final part of a series of articles focusing on Compose Multiplatform. We are building an app for Android, iOS, and Desktop that displays the latest Space Flight news.
This part will focus on the following:
- adding a system share functionality to share links to articles with different apps.
- opening the article URL in an external web browser.

Showcase of the final app.
Recap of the first four parts
This article continues where the fourth part left off, so make sure to start there if you havenโt followed the series.
Building a Space Flight News App with Compose Multiplatform for Android, iOS, and Desktop: Part 4
So far, weโve learned the following:
- how Kotlin Multiplatform works
- sharing UI using Compose Multiplatform on iOS, Android, and Desktop
- loading remote images with Coil
- adding network layer for fetching remote data with Ktor
- adding dependency injection with Koin
- handling loading and error states
- adding a local database and offline support with SQLDelight
- adding an article details screen
- and navigating to it using Compose Navigation.
You can find the code after part 4, and what will be our starting point for this article, here:
GitHub - landomen/KMPSpaceFlightNews at part-4
1. Open the Article in a Web browser
We have a โRead moreโ button at the bottom of the article details screen. Letโs implement the logic behind it to open the article web page in the default external web browser. This will allow our users to read the article in its entirety.
Open ArticleDetailsScreen and get an instance of the UriHandler in the root composable function. This class handles the incoming URI/URL so that itโs correctly opened on all platforms.
Next, letโs implement the onReadMoreClick callback where we currently have a TODO. We pass the article URL to the openUri(String) function of the UriHandler.
@Composable
internal fun ArticleDetailsScreen(
articleId: Long,
onBackClick: () -> Unit,
) {
val viewModel = koinViewModel<ArticleDetailsViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
// NEW: instance of the UriHandler
val uriHandler = LocalUriHandler.current
LaunchedEffect(Unit) {
viewModel.onFetch(articleId)
}
ArticleDetailsScreenContent(
state = state,
onRetryClick = {
viewModel.onFetch(articleId)
},
onBackClick = onBackClick,
onShareClick = { article ->
// TODO Open share sheet
},
onReadMoreClick = { articleUrl ->
// NEW: handle URL
uriHandler.openUri(articleUrl)
}
)
}
Thatโs it! Run the app and check the behavior on Android, iOS, and Desktop. On all three platforms, the article web page is opened in the default external browser, which could be Chrome/Safari or another browser.

Opening the article web page in an external browser on Android.

Opening the article web page in an external browser on iOS.

Opening the article web page in an external browser on desktop.
2. Create a share service
The final feature that we will implement is the ability to share a link to the article through an external app. On Android and iOS, this means opening the system share sheet, where the user can select which app to share it on. On Desktop, this means copying the link to the clipboard, giving the user the option to paste it to their desired app or website.
Trigger share action in the ViewModel
Letโs start by updating our ArticleDetailsScreen to handle the click action on the share button and call the ArticleDetailsViewModel.
Open ArticleDetailsScreen and go to the ArticleDetailsSuccessContent function. It currently accepts a onShareClick: () -> Unit argument that we need to extend to get the Article object. We then need to update the IconButton to call onShareClick(article) and pass in the article object.
@Composable
private fun ArticleDetailsSuccessContent(
article: Article,
onBackClick: () -> Unit,
// NEW: Added Article argument
onShareClick: (Article) -> Unit,
onReadMoreClick: (String) -> Unit,
) {
Column(
...
) {
Box(...) {
AsyncImage(...)
IconButton(...)
// NEW: Updated the onClick function
IconButton(
onClick = { onShareClick(article) },
modifier = Modifier.align(Alignment.TopEnd)
) {
Icon(
Icons.Default.Share,
contentDescription = stringResource(Res.string.share_content_description),
tint = Color.White
)
}
}
Column(
...
) {
...
}
}
}
Next, move to the ArticleDetailsScreenContent function and update the existing onShareClick: () -> Unit argument to accept an Article object.
@Composable
private fun ArticleDetailsScreenContent(
state: ArticleDetailsViewModel.ArticleDetailsViewState,
onRetryClick: () -> Unit,
onBackClick: () -> Unit,
// NEW: Updated to accept an Article
onShareClick: (Article) -> Unit,
onReadMoreClick: (String) -> Unit,
) {
...
}
Then, go to the top-level ArticleDetailsScreen function and update the implementation of the onShareClick function to call the new function on the view model.
@Composable
internal fun ArticleDetailsScreen(
articleId: Long,
onBackClick: () -> Unit,
) {
val viewModel = koinViewModel<ArticleDetailsViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
val uriHandler = LocalUriHandler.current
LaunchedEffect(Unit) {
viewModel.onFetch(articleId)
}
ArticleDetailsScreenContent(
state = state,
onRetryClick = {
viewModel.onFetch(articleId)
},
onBackClick = onBackClick,
// NEW: Updated to call the view model
onShareClick = { article ->
viewModel.onShareClick(article)
},
onReadMoreClick = { articleUrl ->
uriHandler.openUri(articleUrl)
}
)
}
Finally, letโs create a new function in the ArticleDetailsViewModel that will be responsible for triggering the share functionality.
fun onShareClick(article: Article) {
// TODO Trigger the share functionality
}
We will return to this function later, once we have implemented the sharing services.
Define the ShareService interface
Weโll start by defining the interface that we will implement on every device. Create a new interface ShareService inside the composeApp/commonMain/your.package.spaceflightnews/share directory. It should have a single function share() that accepts the title of the article to share and its URL.
interface ShareService {
fun share(title: String, url: String)
}
Implement sharing on Android
Create a new class AndroidShareService inside the composeApp/androidMain/your.package.spaceflightnews/share directory. It should extend ShareService and implement the share(title, url) function.
It needs to have a constructor that accepts a Context to be able to create a chooser intent that will trigger the system share sheet.
The rest of the code is standard Android. Weโre creating an intent and setting the payload, like the title and the text to be shared. Finally, weโre launching the intent, which opens the sheet.
import android.content.Context
import android.content.Intent
import com.landomen.spaceflightnews.R
class AndroidShareService(private val context: Context) : ShareService {
override fun share(title: String, url: String) {
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, title)
putExtra(Intent.EXTRA_TEXT, constructMessage(title = title, url = url))
}
context.startActivity(
Intent.createChooser(
shareIntent,
context.getString(R.string.share_title)
).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}
private fun constructMessage(title: String, url: String): String {
return context.getString(R.string.share_message, title, url)
}
}
Youโll notice weโre referencing two new strings in the code above. Open composeApp/src/androidMain/res/values/strings.xml and add the following two strings:
<string name="share_title">Share article</string>
<string name="share_message">%1$s\n\nRead more: %2$s</string>
Now that we have our Android share service, we need to provide it through Koin. Open AppModule.android.kt and add the AndroidShareService to the graph. Weโll inject it later into the ArticleDetailsViewModel.
actual val platformModule: Module
get() = module {
single<DatabaseDriverFactory> { AndroidDatabaseDriverFactory(context = get()) }
single<ShareService> { AndroidShareService(context = get()) }
}
Implement sharing on iOS
Create a new class IOSShareService inside the composeApp/iosMain/your.package.spaceflightnews/share directory. It should extend ShareService and implement the share(title, url) function.
We then need to use a UIActivityViewController which is a standard way to offer content from your app to other apps. Presenting this ViewController displays the share sheet.
import kotlinx.cinterop.BetaInteropApi
import platform.Foundation.NSString
import platform.Foundation.create
import platform.UIKit.UIActivityViewController
import platform.UIKit.UIApplication
class IOSShareService: ShareService {
@OptIn(BetaInteropApi::class)
override fun share(title: String, url: String) {
val activityItems = listOf(
NSString.create(string = "$title\n\nRead more: $url")
)
val activityViewController =
UIActivityViewController(activityItems = activityItems, applicationActivities = null)
// Get the top-most view controller to present the activity view controller
val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController
rootViewController?.presentViewController(activityViewController, animated = true, completion = null)
}
}
To add the iOS share service to the dependency injection, open AppModule.ios.kt and provide it. Weโll inject it later into the ArticleDetailsViewModel.
actual val platformModule: Module
get() = module {
single<DatabaseDriverFactory> { IOSDatabaseDriverFactory() }
single<ShareService> { IOSShareService() }
}
Implement sharing on Desktop
Create a new class DesktopShareService inside the composeApp/desktopMain/your.package.spaceflightnews/share directory. It should extend ShareService and implement the share(title, url) function.
Implementing share functionality on the Desktop is harder because there is no built-in mechanism like on mobile. Instead, weโll copy the text to be shared to the clipboard and let users share it manually to their desired app or website.
Then, to notify the users that the content was copied to their clipboard, weโre building and showing a simple toast message.
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import javax.swing.JLabel
import javax.swing.JWindow
import javax.swing.SwingConstants
import javax.swing.Timer
class DesktopShareService : ShareService {
override fun share(title: String, url: String) {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val selection = StringSelection("$title\n\nRead more: $url")
clipboard.setContents(selection, selection)
showToastMessage("Copied to clipboard")
}
fun showToastMessage(message: String, durationMillis: Int = 5000) {
val window = JWindow()
val label = JLabel(message, SwingConstants.CENTER).apply {
foreground = Color.WHITE
background = Color(0, 0, 0, 200)
isOpaque = true
font = Font("SansSerif", Font.PLAIN, 14)
preferredSize = Dimension(300, 40)
}
window.contentPane.add(label)
window.pack()
// Position at bottom center of screen
val screenSize = Toolkit.getDefaultToolkit().screenSize
val x = (screenSize.width - window.width) / 2
val y = screenSize.height - window.height - 100
window.setLocation(x, y)
window.isAlwaysOnTop = true
window.isVisible = true
// Auto-hide after timeout
Timer(durationMillis) {
window.isVisible = false
window.dispose()
}.start()
}
}
Finally, we need to add the desktop share service to Koin in the AppModule.desktop.kt to later inject it into the ArticleDetailsViewModel.
actual val platformModule: Module
get() = module {
single<DatabaseDriverFactory> { DesktopDatabaseDriverFactory() }
single<ShareService> { DesktopShareService() }
}
Using the ShareService
Now that we have all three implementations of the ShareService, we can inject it into the ArticleDetailsViewModel and trigger the share functionality.
internal class ArticleDetailsViewModel(
private val repository: ArticlesRepository,
// NEW: Added ShareService
private val shareService: ShareService,
) : ViewModel() {
fun onShareClick(article: Article) {
shareService.share(
title = article.title,
url = article.url
)
}
}
As the final step, open AppModule.kt and pass the ShareService to the ArticleDetailsViewModel.
val appModule = module {
...
viewModel { ArticleDetailsViewModel(repository = get(), shareService = get()) }
}
Thatโs it! Weโre now ready to test our sharing functionality.
Testing sharing
Opening an article and then clicking the โShareโ button in the top right corner of the image will trigger the share functionality. Here is how it looks on all three platforms.
Android
The native share sheet is opened, where users can either copy the text or share it to any app that supports it. Since weโre sharing a simple text, the majority of apps should be able to handle it.

Native share sheet on Android.
iOS
The native share sheet is opened, where users can either copy the text or share it to any app that supports it. Since weโre sharing a simple text, the majority of apps should be able to handle it.

Native share sheet on iOS.
Desktop
On desktop we simply copy the text to the clipboard and show a toast to the user.

Share toast on desktop.
3. Conclusion and thank you!
If you followed to the end, great job! This is the end of the fifth and final part of the series on Kotlin Multiplatform and Compose Multiplatform.
Itโs been a long journey, and you should be proud of yourself for building a fully functioning multiplatform app that runs on Android, iOS, and Desktop.
Throughout the series, weโve learned how to:
- create a Kotlin Multiplatform and Compose Multiplatform project,
- build UI with Compose and Material3,
- share view models across platforms,
- add dependency injection using Koin,
- integrate a network layer using Ktor,
- integrate a database layer using SQLDelight to support offline mode,
- display remote images using Coil,
- navigate between screens using Navigation Compose,
- open a URL in an external web browser,
- and open the system share sheet.
Here is the final product on all three platforms:

Completed app running on Android, showing all the features.

Completed app running on iOS, showing all the features.

Completed app running on Desktop, showing all the features.
You can find the source code for this part here:
GitHub - landomen/KMPSpaceFlightNews at part-5
Resources
- https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-ktor-sqldelight.html
- https://kmp.jetbrains.com/
- https://www.jetbrains.com/compose-multiplatform/
- https://spaceflightnewsapi.net/
Building a Space Flight News App with Compose Multiplatform for Android, iOS, and Desktop: Part 5 was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.
Continue reading...