Skip to main content

Competitive Research: Android SDK Design of Deep-Linking / Attribution Platforms

Web research for the OptoLink native Android SDK. Vendors: Branch, AppsFlyer, Adjust, Singular, Kochava + the post-Firebase-Dynamic-Links landscape. Researched 2026; sources inline. Verified against vendor SDK source code where noted.


1. Deferred deep linking mechanisms on Android (2025/2026)​

1.1 Play Install Referrer API — still the deterministic backbone, NOT deprecated​

  • Verified App Links (autoVerify="true" + assetlinks.json on https://domain/.well-known/assetlinks.json, SHA-256 cert fingerprints) open links directly with no disambiguation dialog; unverified links fall back to the chooser/browser. Verification can be manually re-triggered on Android 12+. https://developer.android.com/training/app-links/verify-applinks
  • New in Android 15+ (API 35): Dynamic App Links — assetlinks.json gained a dynamic_app_link_components relation extension: server-side path/fragment/query matchers and exclusions, periodically fetched and merged with static manifest filters. Devices ≤ Android 14 ignore the extension; malformed rules fall back to manifest-only. Rules can narrow but never expand manifest scope. https://developer.android.com/training/app-links/configure-assetlinks — Google's official announcement (Oct 2025) calls Dynamic App Links "the preferred way to link to content within your app," positioning it as the platform answer to FDL's death for the installed-app case. https://android-developers.googleblog.com/2025/10/dynamic-app-links-elevating-your.html
  • Design implication for OptoLink: host assetlinks.json (with dynamic rules) from the link domain itself — this is exactly what Branch/AppsFlyer custom domains do.

1.3 Fingerprinting fallback​

1.4 Clipboard — dead as a background channel​

1.5 Post-Firebase-Dynamic-Links landscape​


2. SDK init & lifecycle patterns​

ConcernPattern observed (all five vendors)
Init locationApplication.onCreate — every vendor. AppsFlyer: "recommended to initialize in the global Application class … to ensure the SDK can start in any scenario (for example, deep linking)". https://dev.appsflyer.com/hc/docs/integrate-android-sdk
Init shapeConfig-object constructor + start, OR singleton accessor. Branch: Branch.getAutoInstance(this) in Application (https://help.branch.io/developer-hub/docs/android-basic-integration). AppsFlyer V7: init(devKey, …) in Application + explicit start() (start takes no args in V7 — dev key comes from init). https://dev.appsflyer.com/hc/docs/migrate-android-sdk-to-v7 . Adjust: Adjust.initSdk(AdjustConfig(ctx, appToken, env)) (verified in source: Adjust.java). https://github.com/adjust/android_sdk
Auto vs manual session initBranch defaults to AUTO: an ActivityLifecycleObserver fires initSession (network v1/open) when the first activity reaches RESUMED — a failsafe "not to miss tracking sessions." Disabling it is an explicit opt-out (Branch.troubleshooting documents the resulting ERR_BRANCH_ALREADY_INITIALIZED conflicts when apps ALSO init manually). https://help.branch.io/developers-hub/docs/android-troubleshooting . Manual lifecycle style: initSession() in onStart(), closeSession() in onStop(). https://help.branch.io/developers-hub/docs/android-troubleshooting
Session definitionBranch: a session = app launched (cold start or resumed from task manager) and brought to foreground; the v1/open request is tied to foregrounding. Handling a new deep link clears session data and creates a new referred OPEN; sessionBuilder()...reInit() exists for re-init without losing state (in-place activity). https://help.branch.io/apidocs/requestopen , https://help.branch.io/developer-hub/docs/android-advanced-features
Install vs open distinctionResolved server-side, exposed via callbacks: AppsFlyer onConversionDataSuccess(Map) fires on first open and every foreground, with is_first_launch key distinguishing install from open. https://dev.appsflyer.com/hc/docs/conversion-data-android . Branch returns +is_first_session-style flags plus +match_guaranteed in the init params JSONObject. https://help.branch.io/developer-hub/docs/implement-attribution-methods
Foreground/background re-attributionAppsFlyer: conversion data re-delivered whenever app moves to foreground (same callback). https://dev.appsflyer.com/hc/docs/conversion-data-android . Branch: each foreground = new session/open; new Branch link = new referred open. (see session rows above)
Delayed initBranch supports delaying init (e.g., to attach request metadata), with warnings about the auto-init race. https://help.branch.io/developer-hub/docs/android-advanced-features . AppsFlyer V7 introduced an explicit "session control model — you control when to start the first session" (iOS doc; Android aligned via start()). https://dev.appsflyer.com/hc/hc/docs/migrate-ios-sdk-to-v7
Deferred-link deliveryAll vendors deliver DDL through the same init callback (Branch) or a config-registered listener (Adjust OnDeferredDeeplinkResponseListener, Singular SingularLinkHandler, Kochava processDeeplink(null) check) — DDL is not a separate API surface from session init. Sources below in §3.
Out-of-the-box trackingBranch auto-tracks clicks, opens, installs, reinstalls and impressions unless disabled. https://help.branch.io/developer-hub/docs/android-advanced-features

3.1 The two routing styles​

A. Listener with params map (Branch, AppsFlyer, Kochava) — SDK hands the app a key/value payload; the app does routing:

B. Typed result objects (AppsFlyer V7 "Unified Deep Linking", Adjust, Singular):

3.2 Conversion / attribution callbacks​

  • Branch: single init callback carries everything (+match_guaranteed, +is_first_session, campaign data, referring link). No separate "attribution changed" stream in the core flow (state listener exists: BranchReferralStateChangedListener). https://help.branch.io/developer-hub/docs/android-full-reference
  • AppsFlyer: dedicated conversion listener (install) + UDL listener (opens); V7 removed the open-attribution duplication — one pipeline.
  • Adjust: attribution is a first-class object (AdjustAttribution: tracker/network/campaign/creative/adgroup/click label/cost type) delivered on change; also OnLastDeeplinkReadListener. https://github.com/adjust/android_sdk
  • Adjust re-attribution is explicit: Adjust.processDeeplink(AdjustDeeplink, context) (verified in source) — app decides when a deeplink counts as a re-attribution touch.

3.3 Event logging APIs​

  • Branch: BranchEvent(BRANCH_STANDARD_EVENT) for standard enums or BranchEvent(String) for custom; builder setters (incl. revenue, custom data, BranchUniversalObject content attachment) then logEvent(). Verified in source (util/BranchEvent.java). https://github.com/BranchMetrics/android-branch-deep-linking-attribution
  • AppsFlyer: AppsFlyerLib.getInstance().logEvent(context, eventName, Map) with AFInAppEventType constants. https://dev.appsflyer.com/hc/docs/android-sdk-reference-appsflyerlib
  • Adjust: AdjustEvent("eventToken") + setRevenue(double, currency), addCallbackParameter, addPartnerParameter, then static Adjust.trackEvent(event). Verified in source. https://github.com/adjust/android_sdk
  • Takeaway for OptoLink: typed event objects (Adjust-style) give compile-time safety and Swagger-able shapes; string+map (AppsFlyer/Branch) is faster to ship but stringly-typed. Note OptoLink's Node SDK already uses typed link objects — mirror that.

4. Footprint: size, dependencies, minSdk, ProGuard​

SDKminSdkDependencies (hard)Notable optional/compileOnly depsProGuard
Branch 5.21.221 (gradle.properties ANDROID_BUILD_SDK_VERSION_MINIMUM=21)Kotlin stdlib + coroutines, androidx.annotation, androidx.constraintlayout (!), com.android.installreferrer:installreferrer:2.2 (bundled as implementation)GMS ads-identifier (AAID), Huawei OAID + referrer, Samsung referrer, Xiaomi referrer, Play Billing (v8, for purchase validation), androidx.browser (Custom Tabs) — all compileOnly, app opts inConsumer rules are only -dontwarn lines (Huawei/MiUI/Samsung/GMS/billing); keeps nothing by default. Verified in repo: proguard-consumer.txt. https://github.com/BranchMetrics/android-branch-deep-linking-attribution
Adjust 5.8.021 (coreMinSdkVersion=21, compileSdk 37)Core is self-contained (Java)Same store-referrer set + OAID + IMEI + Google LVL + webbridge as separate plugin artifacts — core stays smallShips full consumer keep rules (-keep public class com.adjust.sdk.** \{*;} + GMS/installreferrer keeps). MIT license. https://github.com/adjust/android_sdk
AppsFlyer V7 (7.x)21 (V6 line went to 19) — "SDK V7 requires API level 21 or higher"; also requires Kotlin 2.0+ in the appcom.appsflyer:af-android-sdk (single AAR; pulls play-services-integrity since 6.17.1 — excludable for non-Play distribution)installreferrer 2.2 (app adds), Huawei componentverifysdk, Xiaomi homereferrer; Samsung built-in; play-services-appset for AppSet ID (auto-collect since 6.17.0, disableAppSetId() opt-out)-keep class com.appsflyer.** \{ *; } + kotlin.jvm.internal keep. Also: SDK manifest auto-adds AD_ID permission since 6.8.0 (children apps must revoke it), excludes its prefs from backup via manifest merge (documented tools:replace conflicts), and requires hash-named asset files in the APK or network calls fail. https://dev.appsflyer.com/hc/docs/install-android-sdk-7 , https://dev.appsflyer.com/hc/docs/install-android-sdk
Kochava v721 ("minimum supported Android API level increased from 16 to 21")com.kochava.measurement:base (mavenCentral) + opt-in feature modulesStore-referrer modules etc. — "add the base tracker dependency and any desired feature dependencies"Docs-driven; modular keeps base lean. https://support.kochava.com/articles/sdk-integration/20986-android-sdk-integration/ , https://github.com/Kochava/android-kochavameasurement-releases
Singular 12.15.0not confirmed in accessible docsSingle SDK via custom Maven repo—See https://support.singular.net/hc/en-us/articles/360037581952-Android-SDK-Basic-Integration

Design signals: (1) minSdk 21 is the converged floor (Kochava raised 16→21; AppsFlyer V7 19→21; Branch/Adjust at 21). (2) The modern footprint pattern is lean core + optional modules/compileOnly deps for store referrers & ad IDs — Branch and Kochava and Adjust all do it. (3) Branch's constraintlayout as a required implementation dependency is a bloat red flag — don't ship UI deps from a measurement SDK. (4) Nobody publishes byte-size promises anymore; module structure is the size story.


5. Criticisms & complaints worth designing against​

  1. FTC v. Kochava (2022–2026): FTC sued Kochava for selling geolocation data traceable to sensitive locations (reproductive-health clinics, places of worship, shelters). Motion to dismiss denied Feb 2024; May 2026 stipulated order bans selling/sharing sensitive location data without consent. Lesson: an SDK that monetizes location/behavioral data invites regulatory action — keep OptoLink's SDK to click/deeplink scope. https://www.ftc.gov/news-events/news/press-releases/2022/08/ftc-sues-kochava-selling-data-tracks-people-reproductive-health-clinics-places-worship-other , https://www.ftc.gov/legal-library/browse/cases-proceedings/ftc-v-kochava-inc , https://www.ftc.gov/news-events/news/press-releases/2026/05/ftc-ban-kochava-subsidiary-selling-sensitive-location-data-settle-charges-they-sold-location-data
  2. Privacy research (Norwegian Consumer Council, "Out of Control," 2020): AppsFlyer flagged for "systemic oversharing" — e.g., Tinder transmitting special-category data to AppsFlyer/LeanPlum; part of the GDPR complaint wave vs Grindr/Match Group. Lesson: minimize payload, document exactly what leaves the device. https://storage02.forbrukerradet.no/media/2020/01/2020-01-14-out-of-control-final-version.pdf , https://techcrunch.com/2020/01/14/dating-and-fertility-apps-among-those-snitching-to-out-of-control-adtech-report-finds/
  3. Google Play rejections blamed on attribution SDKs: developers report app updates rejected over Data-safety declarations tied to com.appsflyer:af-android-sdk identifiers collection; Play's SDK-requirements policy makes the app developer responsible for embedded SDK behavior. Lesson: OptoLink SDK should collect the minimum and ship a manifest-merge + Data-safety declaration guide. https://github.com/AppsFlyerSDK/appsflyer-react-native-plugin/issues/517 , https://support.google.com/googleplay/android-developer/answer/13323374
  4. Opaque/obfuscated internals: AppsFlyer requires mysterious hash-named files under assets/com/appsflyer/internal (four files a-…d-) for the SDK to function — unauditable blobs in the app package; combined with -keep com.appsflyer.** this is a common auditor complaint. Lesson: readable, inspectable SDK code; no hidden asset blobs. https://dev.appsflyer.com/hc/docs/install-android-sdk
  5. Auto-init footguns: Branch's automatic session init (RESUMED failsafe) collides with manual init (ERR_BRANCH_ALREADY_INITIALIZED, "SDK already initialized" errors) — a documented support topic. Lesson: pick ONE init story and make the failure mode explicit. https://help.branch.io/developers-hub/docs/android-troubleshooting
  6. Stringly-typed payloads: Branch's JSONObject + AppsFlyer's Map<String,Object> callbacks put the typing burden on the app dev (and produce "why is this key missing" support tickets). AppsFlyer's V7 rewrite toward typed DeepLinkResult is an acknowledgment. (§3 sources)
  7. Integration friction: AppsFlyer's manifest backup-rule exclusions cause documented merge conflicts needing tools:replace edits in every integrating app. Lesson: declare minimal manifest entries; avoid forcing app manifest surgery. https://dev.appsflyer.com/hc/docs/install-android-sdk
  8. Platform-policy drift risk: Play policy prohibits fingerprint-bridging of ad-ID resets and persistent-ID linking; SDKs that silently collect device signals (IP/UA/device model — Branch's snapshot comparison) are one policy update away from breaking their apps. Lesson: make probabilistic matching opt-in and isolated. https://support.google.com/googleplay/android-developer/answer/10144311 , https://developer.android.com/identity/user-data-ids
  9. Vendor-hosted link rot: FDL's Aug-2025 404 apocalypse stranded apps whose links lived on page.link. Lesson for OptoLink: links must live on the customer's own domain (assetlinks.json hosted by/for the customer), so our platform can never orphan their URLs the way Firebase did. https://firebase.google.com/support/dynamic-links-faq
  10. ATT note: ATT itself is iOS-only; the Android analogs are the AAID AD_ID permission (API 33+), user ad-ID resets, and Play's Data-safety declarations — these are where Android "ATT-style" complaints land (see #3, #8). https://dev.appsflyer.com/hc/docs/install-android-sdk

  • DDL pipeline: click → store link with install-referrer payload → first-launch InstallReferrerClient bind (once, with timeout, then endConnection()) → server match → same callback as link opens. Referrer payload = the reliable channel; fingerprinting optional & opt-in.
  • Init: OptoLink.init(context, config) in Application.onCreate; config-object pattern; one auto-session story (recommend Branch-style lifecycle auto-init ON, with a single documented escape hatch).
  • API: typed DeepLinkResult-style callbacks (status + data object), Adjust-style boolean-return DDL hook for app-controlled routing, typed event builder (OptoLinkEvent) — skip the JSONObject/Map legacy style.
  • Packaging: single small core artifact, minSdk 21, zero UI dependencies, consumer ProGuard keep rules shipped, no manifest surprises, no auto permissions.
  • Support dynamic App Links hosting (assetlinks.json with dynamic_app_link_components) from the customer link domain — it's Google's stated preferred mechanism and the vendors' custom-domain model.