Tech 11 min read

Flutter + Adjust SDK: How Attribution Works and Implementation Guide

You want to measure where users who installed your app via ads came from. In that case, you use an MMP (Mobile Measurement Partner) such as Adjust.

This article explains how Adjust works and how to integrate it into a Flutter app.

Assumed environment: A Flutter app that embeds a WebView. The native layer is a thin Flutter wrapper, and the main screens and features (registration, billing, etc.) are shown in a WebView.

What Is Adjust

Adjust is a mobile app ad measurement tool. It mainly provides:

  • Attribution – Identify which ad drove the install
  • Event tracking – Track in‑app actions (purchases, registrations, etc.)
  • Fraud prevention – Protect against ad fraud
  • Deep linking – Route users from ads to specific screens

How Acquisition Tracking Works

Flow

1. ユーザーが広告をクリック

2. Adjustのトラッカーリンクを経由

3. アプリストアへリダイレクト

4. ユーザーがアプリをインストール

5. アプリ起動時にAdjust SDKが初期化

6. Adjustサーバーがクリックとインストールをマッチング

How attribution works

Adjust ties the “click” and the “install” together to determine which ad the user came from. It uses the following signals:

  • Device ID (GAID/IDFA) — most accurate
  • Fingerprinting — a combination of IP address, device info, etc.
  • Google Play Referrer — referrer information from the Play Store

On iOS 14.5 and later, ATT (App Tracking Transparency) consent is required. Without consent, measurement relies on SKAdNetwork.

The link you place in ads has the following format:

https://app.adjust.com/{link_token}?campaign={campaign_name}&adgroup={ad_group}&creative={creative}

Example:

https://app.adjust.com/abc123?campaign=summer_sale&adgroup=twitter_feed&creative=video_01

Parameter hierarchy

LevelParameterPurpose
NetworkLink nameAd network (Google, Facebook, etc.)
Campaigncampaign=Campaign identifier
Ad groupadgroup=Ad group/placement
Creativecreative=Creative identifier

Notes:

  • Parameters must be lowercase only
  • Link level cannot be changed after creation
  • Empty parameters are shown as “unknown”

There are several ways media teams will use links. Be ready for the following patterns.

1. Via ad platforms

Set the tracker URL in the ad console for Google Ads, Facebook/Meta, Twitter/X, and others.

ユーザー → 広告クリック → Adjustリンク → ストア → インストール

You can use platform macros (dynamic parameters) to auto‑insert values such as campaign names.

2. Owned media/blog

Place the link directly in articles or banners.

<a href="https://app.adjust.com/abc123?campaign=blog_post">
  アプリをダウンロード
</a>

3. Influencers/affiliates

Issue individual tracker links per partner to measure separately.

インフルエンサーA用: https://app.adjust.com/abc123?campaign=influencer_a
インフルエンサーB用: https://app.adjust.com/def456?campaign=influencer_b

4. QR codes

Measure traffic from offline ads (flyers, posters, in‑store POP).

QRコード → Adjustリンク → ストア → インストール

Use separate links per print asset to see which medium performs best.

5. Smart banners (website)

Banners shown on your website that encourage installing the app. Adjust provides a Smart Banner feature.

Webサイト訪問 → スマートバナー表示 → タップ → ストア → インストール

Send already‑installed users to a specific screen.

広告クリック → アプリが開く → 特定の画面へ遷移

For new installs, use a deferred deep link so the app can open a specific screen after installation.

What the app needs to do

Regardless of how ads are placed, the app work is the same:

  1. Initialize the Adjust SDK — this alone enables install tracking
  2. Make sure the advertising ID can be obtained — send to your backend if needed
  3. Implement event tracking — to measure conversions such as registration and purchase
  4. Handle deep links (if needed) — to route to specific screens

No matter which distribution pattern the media team uses, the SDK‑side implementation does not change.

Integrating the Flutter SDK

1. Add the package

Add to pubspec.yaml:

dependencies:
  adjust_sdk: ^5.5.0

Install:

flutter pub get

2. Android setup

android/app/build.gradle:

dependencies {
    implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0'
    implementation 'com.android.installreferrer:installreferrer:2.2'
}

android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

3. iOS setup

Add an ATT description to ios/Runner/Info.plist:

<key>NSUserTrackingUsageDescription</key>
<string>パーソナライズされた広告を表示するために使用します</string>

Add these frameworks in Xcode:

  • AdSupport.framework
  • StoreKit.framework
  • AppTrackingTransparency.framework

4. Initialize the SDK

import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_config.dart';

void main() {
  // Adjust初期化
  AdjustConfig config = AdjustConfig(
    '{YourAppToken}', // Adjust管理画面で取得
    AdjustEnvironment.sandbox, // 本番時は AdjustEnvironment.production
  );

  // ログレベル(開発時のみ)
  config.logLevel = AdjustLogLevel.verbose;

  Adjust.initSdk(config);

  runApp(MyApp());
}

Environment switching:

  • During development/testing: AdjustEnvironment.sandbox
  • For production releases: AdjustEnvironment.production

Implementing Event Tracking

For WebView apps there are two approaches:

  1. Track on the Flutter side — notify Flutter from the WebView and call the Adjust SDK from Flutter
  2. Track on the server — call your server API from the WebView, then send events from your server to Adjust’s Server‑to‑Server API

Below we’ll cover tracking on the Flutter side. With server‑side tracking, as long as you send the advertising ID to the server, the server can call the Adjust API directly.

Basic event tracking

Create event tokens in the Adjust dashboard and track in your app:

import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_event.dart';

// ボタンタップなどのイベント
void trackButtonClick() {
  AdjustEvent event = AdjustEvent('{EventToken}');
  Adjust.trackEvent(event);
}

Revenue events (purchase tracking)

void trackPurchase(double amount, String currency, String transactionId) {
  AdjustEvent event = AdjustEvent('{RevenueEventToken}');

  // 金額と通貨を設定
  event.setRevenue(amount, currency);

  // 重複防止用のトランザクションID
  event.deduplicationId = transactionId;

  Adjust.trackEvent(event);
}

// 使用例
trackPurchase(980, 'JPY', 'order_12345');

Add custom parameters

void trackEventWithParams() {
  AdjustEvent event = AdjustEvent('{EventToken}');

  // サーバーコールバック用パラメータ
  event.addCallbackParameter('user_id', '12345');
  event.addCallbackParameter('item_id', 'abc');

  // パートナー連携用パラメータ
  event.addPartnerParameter('product_id', 'xyz');

  Adjust.trackEvent(event);
}

Notify events from the WebView

Notify the Flutter side about events (registration completed, purchase completed, etc.) that occur inside the WebView and track them with the Adjust SDK:

// Flutter側
webViewController = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel(
    'AdjustBridge',
    onMessageReceived: (message) {
      // JSONでイベント情報を受け取る
      final data = jsonDecode(message.message);
      final eventType = data['type'];

      switch (eventType) {
        case 'registration':
          trackRegistration(data['userId']);
          break;
        case 'purchase':
          trackPurchase(
            data['amount'].toDouble(),
            data['currency'],
            data['transactionId'],
          );
          break;
      }
    },
  );

void trackRegistration(String userId) {
  AdjustEvent event = AdjustEvent('{RegistrationEventToken}');
  event.addCallbackParameter('user_id', userId);
  Adjust.trackEvent(event);
}
// Web側(登録完了時)
function onRegistrationComplete(userId) {
  if (window.AdjustBridge) {
    AdjustBridge.postMessage(JSON.stringify({
      type: 'registration',
      userId: userId
    }));
  }
}

// Web側(課金完了時)
function onPurchaseComplete(amount, currency, transactionId) {
  if (window.AdjustBridge) {
    AdjustBridge.postMessage(JSON.stringify({
      type: 'purchase',
      amount: amount,
      currency: currency,
      transactionId: transactionId
    }));
  }
}

Getting Attribution Information

Obtain in‑app information about which ad drove the install:

AdjustConfig config = AdjustConfig(
  '{YourAppToken}',
  AdjustEnvironment.sandbox,
);

// アトリビューションコールバック
config.attributionCallback = (AdjustAttribution attribution) {
  print('Network: ${attribution.network}');
  print('Campaign: ${attribution.campaign}');
  print('Adgroup: ${attribution.adgroup}');
  print('Creative: ${attribution.creative}');
};

Adjust.initSdk(config);

How to Test

1. Check logs

Set AdjustLogLevel.verbose and confirm the following in logs:

  • Install tracked — install tracked successfully
  • Event tracked — event tracked successfully
  • gps_adid — advertising ID is available

2. Check in the Adjust dashboard

  1. Register your device in the Testing Console
  2. Click the test link
  3. Install and launch the app
  4. Confirm in realtime data in the dashboard

3. Sandbox environment

To avoid polluting production data, always use the sandbox environment during development.

Integration With Ad Networks

Integrating with Google Ads

  1. Add the Google Ads network in the Adjust dashboard
  2. Set the link ID in Google Ads
  3. Create conversion actions

Integrating with Facebook/Meta

https://app.adjust.com/abc123?campaign={{campaign.name}}&adgroup={{adset.name}}&creative={{ad.name}}

You can dynamically set parameters using Facebook macros.

Passing the Advertising ID in WebView Apps

When you build an app using Flutter with a WebView, flows like registration are often handled on the web side. In that case, you need to pass the advertising ID obtained on the Flutter side to the WebView.

Get the advertising ID

You can get the advertising ID directly from the Adjust SDK:

import 'dart:io';
import 'package:adjust_sdk/adjust.dart';

Future<String?> getAdvertisingId() async {
  if (Platform.isAndroid) {
    return await Adjust.getGoogleAdId();
  } else if (Platform.isIOS) {
    return await Adjust.getIdfa();
  }
  return null;
}

Method 1: Pass via URL parameter

The simplest approach. Add it as a query parameter to the URL loaded in the WebView.

Future<void> loadWebView() async {
  String? adId = await getAdvertisingId();

  String baseUrl = 'https://example.com/register';
  String url = adId != null
      ? '$baseUrl?ad_id=$adId'
      : baseUrl;

  webViewController.loadRequest(Uri.parse(url));
}

On the web side, get it with URLSearchParams:

const params = new URLSearchParams(window.location.search);
const adId = params.get('ad_id');

// 登録APIに含める
fetch('/api/register', {
  method: 'POST',
  body: JSON.stringify({ advertisingId: adId, ...formData })
});

Pros: Easy to implement; minimal web changes

Cons: Exposed in the URL; must be carried across navigations

Method 2: Pass via JavaScript channel

Execute JavaScript from Flutter to set the advertising ID into a global variable or localStorage.

import 'package:webview_flutter/webview_flutter.dart';

late WebViewController webViewController;

Future<void> initWebView() async {
  String? adId = await getAdvertisingId();

  webViewController = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setNavigationDelegate(
      NavigationDelegate(
        onPageFinished: (url) {
          // ページ読み込み完了後に広告IDを注入
          if (adId != null) {
            webViewController.runJavaScript('''
              window.flutterAdvertisingId = "$adId";
              localStorage.setItem("advertisingId", "$adId");
            ''');
          }
        },
      ),
    )
    ..loadRequest(Uri.parse('https://example.com'));
}

On the web side:

// グローバル変数から取得
const adId = window.flutterAdvertisingId;

// または localStorage から取得
const adId = localStorage.getItem('advertisingId');

Pros: Not exposed in the URL; persists across navigations (localStorage)

Cons: Requires timing control (inject after page load)

Method 3: Two‑way via JavaScript channel

Let the web page request the advertising ID from Flutter.

Future<void> initWebView() async {
  webViewController = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..addJavaScriptChannel(
      'FlutterBridge',
      onMessageReceived: (message) async {
        if (message.message == 'getAdvertisingId') {
          String? adId = await getAdvertisingId();
          webViewController.runJavaScript(
            'window.onAdvertisingIdReceived("${adId ?? ""}")'
          );
        }
      },
    )
    ..loadRequest(Uri.parse('https://example.com'));
}

Web side:

// 広告IDをリクエスト
function requestAdvertisingId() {
  if (window.FlutterBridge) {
    FlutterBridge.postMessage('getAdvertisingId');
  }
}

// コールバックで受け取る
window.onAdvertisingIdReceived = function(adId) {
  console.log('Received advertising ID:', adId);
  // 登録処理に使用
};

// ページ読み込み時にリクエスト
document.addEventListener('DOMContentLoaded', requestAdvertisingId);

Pros: Web can fetch it when needed; flexible

Cons: More complex; adds Flutter‑specific code to the web

Method 4: Include in the User‑Agent

Include the advertising ID in a custom User‑Agent and parse it on the server.

Future<void> initWebView() async {
  String? adId = await getAdvertisingId();
  String customUserAgent = 'MyApp/1.0 AdId/${adId ?? "unknown"}';

  webViewController = WebViewController()
    ..setUserAgent(customUserAgent)
    ..loadRequest(Uri.parse('https://example.com'));
}

Server side (example: Node.js):

app.post('/api/register', (req, res) => {
  const userAgent = req.headers['user-agent'];
  const match = userAgent.match(/AdId\/([^\s]+)/);
  const adId = match ? match[1] : null;

  // 登録処理
});

Pros: Automatically included with every request; no web JS changes

Cons: Requires parsing on the server

Method 5: Use cookies

Set a cookie from Flutter on first access and read it on the web.

import 'package:webview_flutter/webview_flutter.dart';

Future<void> initWebView() async {
  String? adId = await getAdvertisingId();

  final cookieManager = WebViewCookieManager();

  if (adId != null) {
    await cookieManager.setCookie(
      WebViewCookie(
        name: 'advertising_id',
        value: adId,
        domain: 'example.com',
        path: '/',
      ),
    );
  }

  webViewController = WebViewController()
    ..loadRequest(Uri.parse('https://example.com'));
}

Web side:

function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

const adId = getCookie('advertising_id');

Pros: Usable across the domain; persists across navigations

Cons: Be mindful of cookie limits (size, SameSite, etc.)

Which method to choose

MethodImplementation effortWeb changesRecommended cases
URL parameterEasyLowRegistration completes on a single page
JS injectionMediumMediumSPA, includes page transitions
Two‑way channelHigherHighWhen complex integration is required
User‑AgentEasyNoneWhen you prefer server‑side handling
CookieMediumLowWhen you want to share across multiple pages/domains

Summary

The basic flow for measuring ad acquisition with Adjust:

  1. Register your app in the Adjust dashboard and get the app token
  2. Create tracker links and place them in ads
  3. Integrate and initialize the Flutter SDK
  4. Implement event tracking as needed
  5. Test in the sandbox environment
  6. Switch to production and release

As a next step, register your app in the Adjust dashboard and try testing in the sandbox environment.

References