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
Applicationclass - Android manifest configured with intent filters for your domain
ULink Dashboard Configuration
- 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.
Understanding Link Reception
The ULink SDK provides two main ways to receive links:
- Automatic Handling (Recommended): The SDK automatically processes incoming intents when
enableDeepLinkIntegrationis enabled - 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.
1. Automatic Handling (Recommended)
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)
}
}
Step 1.2: Observe Link Streams
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
}
}
dynamicLinkStream: Emits dynamic links with parameters for in-app navigationunifiedLinkStream: Emits unified links for marketing/redirect scenarios- Both streams emit
ULinkResolvedDataobjects containing the resolved link information
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
lifecycleScopeautomatically 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)
}
}
}
}
- 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
3. Access Initial and Last Links
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 Initial Deep Link
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)
}
}
Get Last Link Data
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)
}
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
- User clicks a ULink but doesn't have the app installed
- User is redirected to Google Play and installs the app
- User opens the app for the first time
- SDK retrieves the original link data and delivers it via the link streams
Automatic Deferred Link Handling
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)
}
}
Link Data Properties for Deferred Links
| Property | Type | Description |
|---|---|---|
isDeferred | Boolean | true if this link came from deferred deep linking |
matchType | String? | Match method: "deterministic" or "fingerprint" |
Manual Deferred Link Check
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()
- 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
- One-Time Check: Deferred link check only runs once per app installation
- 24-Hour Expiry: Deferred link data expires 24 hours after the original click
- Network Required: The check requires an active network connection
- 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)
}
}
5. Verify App Links Configuration
Use Android's verification tools to confirm the OS trusts your domain:
Check App Links Status
# 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 Deep Links
# 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
Link Handling
- Always check for initial links: Use
getInitialDeepLink()when your Activity starts to handle links that launched the app - Use lifecycle-aware scopes: Always use
lifecycleScopeorviewModelScopeto avoid memory leaks - Handle both stream types: Set up listeners for both
dynamicLinkStreamandunifiedLinkStream - Validate parameters: Always validate and safely cast parameters before using them
Navigation
- 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
Links Not Opening App
- 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
enableDeepLinkIntegrationis 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
- Create Links → Android - Learn how to create links programmatically
- Troubleshoot & Test Deep Links - Debug and verify your links