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.
Structure of a Tracker URL (Link)
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
| Level | Parameter | Purpose |
|---|---|---|
| Network | Link name | Ad network (Google, Facebook, etc.) |
| Campaign | campaign= | Campaign identifier |
| Ad group | adgroup= | Ad group/placement |
| Creative | creative= | Creative identifier |
Notes:
- Parameters must be lowercase only
- Link level cannot be changed after creation
- Empty parameters are shown as “unknown”
How ad links are distributed
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サイト訪問 → スマートバナー表示 → タップ → ストア → インストール
6. Deep links (for existing users)
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:
- Initialize the Adjust SDK — this alone enables install tracking
- Make sure the advertising ID can be obtained — send to your backend if needed
- Implement event tracking — to measure conversions such as registration and purchase
- 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.frameworkStoreKit.frameworkAppTrackingTransparency.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:
- Track on the Flutter side — notify Flutter from the WebView and call the Adjust SDK from Flutter
- 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 successfullyEvent tracked— event tracked successfullygps_adid— advertising ID is available
2. Check in the Adjust dashboard
- Register your device in the Testing Console
- Click the test link
- Install and launch the app
- 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
- Add the Google Ads network in the Adjust dashboard
- Set the link ID in Google Ads
- 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
| Method | Implementation effort | Web changes | Recommended cases |
|---|---|---|---|
| URL parameter | Easy | Low | Registration completes on a single page |
| JS injection | Medium | Medium | SPA, includes page transitions |
| Two‑way channel | Higher | High | When complex integration is required |
| User‑Agent | Easy | None | When you prefer server‑side handling |
| Cookie | Medium | Low | When you want to share across multiple pages/domains |
Summary
The basic flow for measuring ad acquisition with Adjust:
- Register your app in the Adjust dashboard and get the app token
- Create tracker links and place them in ads
- Integrate and initialize the Flutter SDK
- Implement event tracking as needed
- Test in the sandbox environment
- Switch to production and release
As a next step, register your app in the Adjust dashboard and try testing in the sandbox environment.