Make Your KMP App a Share Target: Receive simple data from other applications

What is a Share Target?
Let’s say you’re developing a social media app. You might want to provide the ability to share text or images. For that, Android has a feature called Share Target (similar to Share Extension in iOS). This allows your app to receive text or images shared from another app.
For example: When you share an image from your gallery to a social media app like WhatsApp, a share sheet opens showing different app icons (like WhatsApp, Instagram, etc.), and you can select where to share the image.
In this Article we going to implement this step by step.
Step 1: In Android Declare Your Activity as a Share Target
Open your existing Kotlin Multiplatform Project or create new one using Kotlin Multiplatform Wizard. In your project go to androidMain SourceSet in AndroidManifest.xml, add intent filters to register your activity as a receiver for share actions. This allows your app to appear in the Android Sharesheet.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".DcnYTLoader"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:allowTaskReparenting="true"
android:exported="true"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- from here -->
<intent-filter
android:label="@string/share_label">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter
android:label="@string/share_label">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<!-- to here -->
</activity>
</application>
</manifest>
Step 2: Handling Incoming Share Intents
Now go to commonMain and in App.kt or where you app's entrypoint function exists. In NavHost add listof navDeepLink
in composable function of the feature or screen where you want to receive shared data.
@OptIn(KoinExperimentalAPI::class)
@Composable
fun App(navController: NavHostController = rememberNavController()) {
KoinMultiplatformApplication(config = initKoin()) {
AppTheme {
NavHost(navController = navController, startDestination = Home) {
composable<Home>(
deepLinks =
listOf(
navDeepLink {
action = "android.intent.action.SEND"
mimeType = "text/*"
},
navDeepLink {
action = "android.intent.action.SEND"
mimeType = "image/*"
},
),
) {
Column(
Modifier.fillMaxSize(),
) {
DownloadHistoryScreen()
}
}
}
}
}
}
Step 3: ViewModel – Centralizing Shared Content Handling
we going to use ViewModel with Koin DI, extracting parameters from the SavedStateHandle (which contains the incoming data):
first define an interface and classes for data :
sealed interface SharedContent {
data class SharedTxtContent(
val textContent: String,
val contentType: ContentType = ContentType.PLAIN_TEXT,
) : SharedContent
data class SharedImageContent(
val imageBytes: ByteArray,
val contentType: ContentType = ContentType.IMAGE,
) : SharedContent {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SharedImageContent
if (!imageBytes.contentEquals(other.imageBytes)) return false
if (contentType != other.contentType) return false
return true
}
override fun hashCode(): Int {
var result = imageBytes.contentHashCode()
result = 31 * result + contentType.hashCode()
return result
}
}
data object EmptyContent : SharedContent
}
enum class ContentType {
IMAGE,
PLAIN_TEXT,
EMPTY,
}
class MainViewModel(
private val ytdlService: YtdlService,
private val notificationManager: DownloadNotificationManager,
savedStateHandle: SavedStateHandle,
) : ViewModel(),
KoinComponent {
val sharedContentUiState =
savedStateHandle
.handleShareIntent(platformContext)
.map {
when (it) {
SharedContent.EmptyContent ->
ShareIntentUiState.NoData(
ContentType.EMPTY,
"error",
)
is SharedContent.SharedImageContent ->
ShareIntentUiState.HasImage(
ContentType.IMAGE,
"success",
it.imageBytes,
)
is SharedContent.SharedTxtContent ->
ShareIntentUiState.HasTxt(
ContentType.PLAIN_TEXT,
"success",
it.textContent,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds),
initialValue = ShareIntentUiState.NoData(ContentType.EMPTY, "error"),
)
...
Here The handleShareIntent
extension function inspects the intent for Intent.EXTRA_TEXT
or Intent.EXTRA_STREAM
, maps the data, and exposes it reactively to your UI.
in CommonMain:
expect fun SavedStateHandle.handleShareIntent(context: PlatformContext): Flow<SharedContent>
HandleShareIntent.kt
in androidMain:
actual fun SavedStateHandle.handleShareIntent(context: PlatformContext): Flow<SharedContent> =
getStateFlow(
NavController.KEY_DEEP_LINK_INTENT,
Intent().also {
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_ACTIVITY_CLEAR_TOP
},
).map { intent -> intent.parseSharedContent(context) }
HandleShareIntent.android.kt
What is PlatformContext
here? Well it is Just an expect abstract class we need it for parseSharedContent
to pass android context. it will be impliment by each target platform. you can define it like this.
expect abstract class PlatformContext // in commonMain
actual typealias PlatformContext = Context // in androidMain
actual abstract class PlatformContext private constructor() { // on nonAndroid(jvmMain or nativeMain)
companion object {
val INSTANCE = object : PlatformContext() {}
}
}
parseSharedContent
extension function extract shared content from an Intent. It takes an optional Context
as a parameter. The context is needed to access the ContentResolver if the shared content is an image URI. And It returns a SharedContent
object.
fun Intent.parseSharedContent(context: Context?): SharedContent {
if (action != Intent.ACTION_SEND) return SharedContent.EmptyContent
return if (isTextMimeType()) {
val textContent = getStringExtra(Intent.EXTRA_TEXT) ?: ""
SharedContent.SharedTxtContent(textContent, ContentType.PLAIN_TEXT)
} else if (isImageMimeType()) {
val imageContent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelableExtra(Intent.EXTRA_STREAM, Parcelable::class.java) as? Uri
} else {
@Suppress("DEPRECATION")
getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
}
imageContent?.let {
val bytes =
context?.contentResolver?.openInputStream(it)?.use { input ->
input.readBytes()
} ?: byteArrayOf()
SharedContent.SharedImageContent(bytes, ContentType.IMAGE)
} ?: SharedContent.EmptyContent
} else {
SharedContent.EmptyContent
}
}
Step 4: UI – React on Shared Content
With the view state, Compose can now conditionally show content or an error if nothing was received.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DownloadHistoryScreen(
modifier: Modifier = Modifier,
viewModel: MainViewModel = koinViewModel(),
) {
val sharedContentUiState by viewModel.sharedContentUiState.collectAsStateWithLifecycle()
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(sharedContentUiState.type) {
when (val it = sharedContentUiState) {
is ShareIntentUiState.HasTxt -> {
openBottomSheet = true
println("text received in ui state : ${it.txt}")
}
is ShareIntentUiState.HasImage -> {
openBottomSheet = true
println("image received in ui state : ${it.image}")
}
is ShareIntentUiState.NoData -> {
println("no data received in ui state")
}
}
}
Bonus Tip
You can make your app even better: By default, when you receive data from another app using your current configuration, a new instance of your activity is created every time. If you prefer to use the same activity instance, add this line to your activity tag in the AndroidManifest.xml file:android:launchMode="singleInstance"
However, this change introduces a new challenge. With singleInstance
mode, your activity only receives data from the share intent the first time it launches fresh. On subsequent resumes, Compose Navigation will not automatically detect new share intents. To handle this, you must manually process incoming intents in your Android activity using the appropriate lifecycle methods. For example, handle new intents in onNewIntent()
to ensure shared data is received every time.
class MainActivity : ComponentActivity() {
private lateinit var navController: NavHostController // step 1
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
navController = rememberNavController() //step 2
App(navController) //step 3
}
}
override fun onNewIntent(intent: Intent) { // step 4
super.onNewIntent(intent)
navController.handleDeepLink(intent)
}
}
Conclusion
By following the steps outlined above—configuring your AndroidManifest, handling intents in your activity, and centralizing your business logic within a shared ViewModel—you can enable native share target functionality in your Kotlin Multiplatform app for Android. This approach keeps your architecture clean, organized, and easily extensible for other platforms.
With the Android part now complete, we’ll explore implementing share target support for iOS in the next article.
If you’d like to stay updated, make sure to subscribe to the newsletter and never miss new guides or tips!
Have questions or want to discuss your ideas? Feel free to comment below or reach out.