Skip to main content

Receive Links on Android

Handle incoming deep links in your Android app using the ULink SDK's reactive streams and intent handling APIs.

Prerequisites

Before receiving links, ensure you've completed the SDK setup:

SDK Integration

  • SDK installed and added to your project dependencies
  • ULink SDK initialized in your Application class
  • Android manifest configured with intent filters for your domain
  • Project settings configured in the ULink dashboard (package name, domain, etc.)
  • API key generated from Dashboard → Settings → API Keys → Generate API Key
  • Domain configured in your project (Domains → Add Domain)

If you haven't completed these steps, follow the Android Getting Started guide to set up the SDK and configure your project.

The ULink SDK provides two main ways to receive links:

  1. Automatic Handling (Recommended): The SDK automatically processes incoming intents when enableDeepLinkIntegration is enabled
  2. Manual Handling: You manually forward intents to the SDK for processing

Both approaches use Kotlin Flows to emit resolved link data that you can observe and react to.

When enableDeepLinkIntegration is enabled in your ULinkConfig (default: true), the SDK automatically handles deep links through Activity lifecycle callbacks. You just need to listen to the streams.

Step 1.1: Initialize SDK with Automatic Integration

In your Application class:

import android.app.Application
import ly.ulink.sdk.ULink
import ly.ulink.sdk.models.ULinkConfig

class MyApp : Application() {
override fun onCreate() {
super.onCreate()

val config = ULinkConfig(
apiKey = "your-api-key",
baseUrl = "https://api.ulink.ly",
debug = true,
enableDeepLinkIntegration = true // Enable automatic handling
)

ULink.initialize(this, config)
}
}

In your Activity, listen to the streams:

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ly.ulink.sdk.ULink
import ly.ulink.sdk.models.ULinkResolvedData

class MainActivity : AppCompatActivity() {
private lateinit var ulink: ULink

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Get the ULink instance (already initialized in Application class)
ulink = ULink.getInstance()

// Set up stream listeners
observeDeepLinks()
}

private fun observeDeepLinks() {
// Listen for dynamic links
lifecycleScope.launch {
ulink.dynamicLinkStream.collect { linkData ->
handleDynamicLink(linkData)
}
}

// Listen for unified links
lifecycleScope.launch {
ulink.unifiedLinkStream.collect { linkData ->
handleUnifiedLink(linkData)
}
}
}

private fun handleDynamicLink(data: ULinkResolvedData) {
Log.d("ULink", "Received dynamic link:")
Log.d("ULink", " Slug: ${data.slug}")
Log.d("ULink", " Parameters: ${data.parameters}")

// Navigate based on parameters
val screen = data.parameters?.get("screen") as? String
if (screen != null) {
navigateToScreen(screen, data.parameters)
}
}

private fun handleUnifiedLink(data: ULinkResolvedData) {
Log.d("ULink", "Received unified link:")
Log.d("ULink", " Slug: ${data.slug}")
// Unified links typically don't have parameters
// Handle marketing/redirect links here
}
}
Stream Types
  • dynamicLinkStream: Emits dynamic links with parameters for in-app navigation
  • unifiedLinkStream: Emits unified links for marketing/redirect scenarios
  • Both streams emit ULinkResolvedData objects containing the resolved link information
Coroutine Scope

Since streams use Kotlin Flows, you must collect them in a coroutine scope:

  • In Activities/Fragments: Use lifecycleScope.launch
  • In ViewModels: Use viewModelScope.launch
  • The lifecycleScope automatically cancels when the lifecycle is destroyed

2. Manual Handling

If you prefer manual control or need to process intents before SDK handling:

Step 2.1: Handle Intents Manually

class MainActivity : AppCompatActivity() {
private lateinit var ulink: ULink

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

ulink = ULink.getInstance()

// Handle initial intent
handleIntent(intent)

// Set up stream listeners
observeDeepLinks()
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // Update the activity's intent
handleIntent(intent)
}

private fun handleIntent(intent: Intent?) {
intent?.data?.let { uri ->
// Process URI manually if needed
// ... your custom logic ...

// Forward to SDK
ulink.handleDeepLink(uri)
}
}

private fun observeDeepLinks() {
lifecycleScope.launch {
ulink.dynamicLinkStream.collect { linkData ->
handleDynamicLink(linkData)
}
}

lifecycleScope.launch {
ulink.unifiedLinkStream.collect { linkData ->
handleUnifiedLink(linkData)
}
}
}
}
When to Use Manual Handling
  • You need to validate or transform URIs before processing
  • You want to handle certain intents differently
  • You're integrating with other deep linking frameworks
  • You need more control over when links are processed

The SDK provides methods to access links that arrived before your app was ready or after a cold start.

Get Initial URI

Get the raw URI that launched your app (synchronous):

val initialUri = ulink.getInitialUri()
if (initialUri != null) {
Log.d("ULink", "App launched from: $initialUri")
// Process the initial URI
ulink.handleDeepLink(initialUri)
}

Get the resolved initial deep link data (requires coroutine scope):

lifecycleScope.launch {
val initialDeepLink = ulink.getInitialDeepLink()
if (initialDeepLink != null) {
Log.d("ULink", "Initial link: ${initialDeepLink.slug}")
// Navigate to the appropriate screen
handleDynamicLink(initialDeepLink)
}
}

Retrieve the last resolved link data (useful for resuming state after app restarts):

val lastLink = ulink.getLastLinkData()
if (lastLink != null) {
Log.d("ULink", "Last link: ${lastLink.slug}")
// Use lastLink to restore app state
handleDynamicLink(lastLink)
}
Persistence Required

getLastLinkData() only works when persistLastLinkData is enabled in your ULinkConfig (enabled by default for the native Android SDK). Turn it on if you need to reference the last resolved deep link after cold starts.

4. Deferred Deep Linking

Deferred deep linking allows you to deep link users to specific content even if they haven't installed your app yet. When they install and open the app for the first time, the original deep link is preserved and delivered.

How It Works

  1. User clicks a ULink but doesn't have the app installed
  2. User is redirected to Google Play and installs the app
  3. User opens the app for the first time
  4. SDK retrieves the original link data and delivers it via the link streams

By default, the SDK automatically checks for deferred links on first launch:

ULink.initialize(this, ULinkConfig(
apiKey = "your-api-key",
autoCheckDeferredLink = true // Default: true
))

// Deferred links are delivered via the same streams as regular links
lifecycleScope.launch {
ulink.dynamicLinkStream.collect { linkData ->
// Check if this is a deferred link
if (linkData.isDeferred) {
Log.d("ULink", "Deferred link received!")
Log.d("ULink", "Match type: ${linkData.matchType}")
// matchType can be:
// - "deterministic" (100% accurate, via Play Store referrer)
// - "fingerprint" (probabilistic matching)
}
handleLink(linkData)
}
}
PropertyTypeDescription
isDeferredBooleantrue if this link came from deferred deep linking
matchTypeString?Match method: "deterministic" or "fingerprint"

For more control, disable automatic checking and call checkDeferredLink() manually:

ULink.initialize(this, ULinkConfig(
apiKey = "your-api-key",
autoCheckDeferredLink = false // Disable automatic check
))

// Later, after user completes onboarding or grants permissions:
ulink.checkDeferredLink()
When to Use Manual Check
  • You want to complete onboarding before handling deep links
  • You need user consent before making network requests
  • You want more control over timing

How Matching Works (Android)

The Android SDK uses two methods to match deferred links:

1. Deterministic Matching (Play Store Referrer)

  • Uses Google Play Install Referrer API
  • 100% accurate matching
  • Works for Play Store installations

2. Fingerprint Matching (Fallback)

  • Uses device characteristics (screen resolution, timezone, language, device model)
  • 70-90% accuracy
  • Used when deterministic matching isn't available (sideloaded APKs, etc.)

The SDK automatically uses the best available method. The matchType property tells you which method was used.

Limitations

  1. One-Time Check: Deferred link check only runs once per app installation
  2. 24-Hour Expiry: Deferred link data expires 24 hours after the original click
  3. Network Required: The check requires an active network connection
  4. Play Store Only: Deterministic matching only works for Play Store installs

5. Complete Example

Here's a complete example showing how to set up link reception in an Android app:

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ly.ulink.sdk.ULink
import ly.ulink.sdk.models.ULinkResolvedData

class MainActivity : AppCompatActivity() {
private lateinit var ulink: ULink

companion object {
private const val TAG = "MainActivity"
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Get the ULink instance (already initialized in Application class)
ulink = ULink.getInstance()

// Set up stream listeners
observeDeepLinks()

// Check for initial link
lifecycleScope.launch {
val initialLink = ulink.getInitialDeepLink()
if (initialLink != null) {
handleLink(initialLink)
}
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // Update the activity's intent
// With automatic integration, this is handled automatically
// But you can still manually handle if needed
}

private fun observeDeepLinks() {
lifecycleScope.launch {
ulink.dynamicLinkStream.collect { linkData ->
handleLink(linkData)
}
}

lifecycleScope.launch {
ulink.unifiedLinkStream.collect { linkData ->
handleUnifiedLink(linkData)
}
}
}

private fun handleLink(data: ULinkResolvedData) {
Log.d(TAG, "Received link: ${data.slug}")

// Navigate based on link data
val screen = data.parameters?.get("screen") as? String
when (screen) {
"product" -> {
val productId = data.parameters?.get("productId") as? String
if (productId != null) {
navigateToProduct(productId)
}
}
"profile" -> {
val userId = data.parameters?.get("userId") as? String
if (userId != null) {
navigateToProfile(userId)
}
}
else -> {
// Default navigation or handle unknown screens
Log.d(TAG, "Unknown screen: $screen")
}
}
}

private fun handleUnifiedLink(data: ULinkResolvedData) {
// Handle marketing/redirect links
Log.d(TAG, "Unified link received: ${data.slug}")
}

private fun navigateToProduct(productId: String) {
// Your navigation logic
val intent = Intent(this, ProductActivity::class.java).apply {
putExtra("productId", productId)
}
startActivity(intent)
}

private fun navigateToProfile(userId: String) {
// Your navigation logic
val intent = Intent(this, ProfileActivity::class.java).apply {
putExtra("userId", userId)
}
startActivity(intent)
}
}

Use Android's verification tools to confirm the OS trusts your domain:

# Force Android to re-check Digital Asset Links
adb shell pm verify-app-links --re-verify your.package.name

# Inspect current status for each host
adb shell pm get-app-links your.package.name

Status VERIFIED: true indicates the domain is properly associated. If it's false, check your Digital Asset Links JSON or make sure your manifest intent filter matches the host/scheme.

# Test with ADB
adb shell am start \
-a android.intent.action.VIEW \
-d "https://links.shared.ly/slug"

Verify Logcat contains Handling intent with URI and confirm your collector receives the payload.

Best Practices

  • Always check for initial links: Use getInitialDeepLink() when your Activity starts to handle links that launched the app
  • Use lifecycle-aware scopes: Always use lifecycleScope or viewModelScope to avoid memory leaks
  • Handle both stream types: Set up listeners for both dynamicLinkStream and unifiedLinkStream
  • Validate parameters: Always validate and safely cast parameters before using them
  • Centralize navigation logic: Create a single method to handle all link-based navigation
  • Handle edge cases: Account for missing parameters or invalid link data
  • Provide fallbacks: If navigation fails, show an appropriate error or fallback screen
  • Use Intent extras: Pass link data to target activities via Intent extras

Testing

  • Test both URL types: Test both custom schemes (myapp://) and App Links (https://)
  • Test cold starts: Ensure links work when the app is launched from a closed state
  • Test warm starts: Ensure links work when the app is already running
  • Check logs: Enable debug mode and monitor Logcat for link processing

Troubleshooting

  • Verify intent filters are configured in AndroidManifest.xml
  • Check that android:autoVerify="true" is set for App Links
  • Verify domain matches dashboard configuration
  • Ensure Digital Asset Links JSON is accessible and valid

Streams Not Emitting

  • Confirm SDK initialization completed successfully
  • Check that enableDeepLinkIntegration is enabled
  • Verify intents are being forwarded to handleDeepLink (if using manual handling)
  • Enable debug mode to see processing logs
  • Ensure you're collecting streams in a coroutine scope

Parameters Missing

  • Verify link was created with parameters
  • Check that you're listening to the correct stream (dynamic vs unified)
  • Ensure parameters are accessed safely with null checks and type casting

Next Steps