DEV Community

Michael Otieno Olang
Michael Otieno Olang

Posted on

Meta App Events in Flutter for Android: Setup, Tracking, and Ad Attribution

A practical walkthrough of Meta App IDs, Android SDK setup, booking funnel events, and testing app campaign conversions.

I recently added Meta App Events to a Flutter booking app so we could measure app installs, see where people leave the booking flow, and eventually optimize Meta campaigns for paid tickets. The SDK was sending events, but one question remained: if my ad account has a different ID from my app, how does Meta connect a ticket sale to an ad?

This is the setup and testing guide I wish I had while working through it. It covers Android app events. An iOS build needs its own SDK and measurement setup; adding Android code does not enable iOS tracking.

First, identify the three IDs

ID What it identifies Where it belongs
Meta App ID The app registered in Meta for Developers Android SDK configuration and the app selected in an app campaign
Ad Account ID The account that owns and pays for campaigns Meta Ads Manager and business asset access; it does not go in the mobile SDK
Pixel ID A website tracking source Website Pixel or web Conversions API setup, if you measure website activity

It is normal for all three numbers to differ. The important comparison is the Meta App ID inside the Android app versus the App ID selected for the campaign. If those point to different Meta apps, the campaign will not use the events from the SDK integration I tested.

Ad account runs an app campaign and selects a Meta app
                         ↓
Someone interacts with that ad and installs or opens the app
                         ↓
The app's SDK sends Activate App, Search, Purchase, etc. to its Meta App ID
                         ↓
Meta applies its attribution rules to eligible ad interactions and app events
Enter fullscreen mode Exit fullscreen mode

The app sends events to Meta, not directly to the ad account. Meta already knows which account ran the campaign. An event arriving in Events Manager proves that event collection works; it does not, by itself, prove a campaign received credit for the event.

What I needed before writing code

  • A Meta for Developers app for the real mobile app, with its App ID and Client Token.
  • The Android package name and launch activity from the production app, plus its Play Store listing.
  • The Android signing key hashes for builds I would test and distribute.
  • A Meta business/ad account that can access the registered app.
  • A test Android device and a build containing the SDK.

I kept the App ID and Client Token from the same Meta app. The website Pixel ID was not used in Android configuration.

Facebook Login is not required for App Events. The booking app can report opens and purchases without asking customers to sign in with Facebook.

For my TRAVLER Android build, the production package is com.ma3app, the launch activity is com.ma3app.MainActivity, and the SDK App ID is 1320645970220890. I compare that last number with the app selected in Ads Manager. The Client Token comes from the root .env at build time; its value does not belong in this post.

Step 1: Register the Android app with Meta

In Meta for Developers → My Apps, open the intended app. Under its app settings, add the Android platform and enter the app's package name, the fully qualified activity that opens it, and the relevant store details. In this Flutter project, the package and activity are represented by applicationId in android/app/build.gradle.kts and MainActivity in the Android source. Verify them from the release build rather than guessing from a sample manifest.

Copy the App ID from the developer app settings. Find the Client Token in that same app's advanced/security settings. Meta's menu names can move, so the important check is the app shown at the top of each settings page.

Signing key hashes

Meta's Android platform configuration asks for Android key hashes. Add the development hash for local debug builds and the hash of the certificate that signs the app users actually download. If Google Play App Signing signs the distributed APKs, use its App Signing certificate, not just your upload key.

For the standard Android debug keystore, a key hash can be calculated with:

keytool -exportcert -alias androiddebugkey \
  -keystore "$HOME/.android/debug.keystore" \
  -storepass android -keypass android \
  | openssl sha1 -binary | openssl base64
Enter fullscreen mode Exit fullscreen mode

For an exported DER certificate from Play Console:

openssl x509 -inform DER -in play_app_signing.der -outform DER \
  | openssl sha1 -binary | openssl base64
Enter fullscreen mode Exit fullscreen mode

The result is a Base64 SHA-1 key hash. It is different from the SHA-256 fingerprint used in Android App Links' assetlinks.json. Those two values solve different problems and cannot be substituted for each other.

Step 2: Add the Android SDK to Flutter's native project

The Flutter UI does not initialize Meta's Android SDK by itself. I added Meta's core Android dependency to android/app/build.gradle.kts:

dependencies {
    implementation("com.facebook.android:facebook-core:19.0.0")
}
Enter fullscreen mode Exit fullscreen mode

19.0.0 is the version this project was built and tested with; check Meta's current release before using the snippet in a new project. Gradle must be able to resolve it from Maven Central.

The project's root .env contains the client token:

META_CLIENT_TOKEN=YOUR_CLIENT_TOKEN_FROM_THE_SAME_META_APP
Enter fullscreen mode Exit fullscreen mode

Flutter's Dart .env loader does not automatically place that value into Android resources. The native Gradle build reads the file and generates a string resource. A shortened version of the approach is:

val metaClientToken = rootProject.file("../.env").readLines()
    .first { it.trimStart().startsWith("META_CLIENT_TOKEN=") }
    .substringAfter("=").trim().trim('"', '\'')

android {
    defaultConfig {
        resValue("string", "facebook_client_token", metaClientToken)
    }
}
Enter fullscreen mode Exit fullscreen mode

In production code, fail the build if the value is missing and support an environment variable in CI. Keep .env out of Git. This keeps the token out of source control, but it is still packaged into the Android app; a local .env is not a secret vault for values shipped to a device.

I put the App ID in android/app/src/main/res/values/strings.xml:

<resources>
    <string name="facebook_app_id">YOUR_META_APP_ID</string>
</resources>
Enter fullscreen mode Exit fullscreen mode

Then I added the metadata inside <application> in android/app/src/main/AndroidManifest.xml:

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

<application ...>
    <meta-data
        android:name="com.facebook.sdk.ApplicationId"
        android:value="@string/facebook_app_id" />
    <meta-data
        android:name="com.facebook.sdk.ClientToken"
        android:value="@string/facebook_client_token" />
    <meta-data
        android:name="com.facebook.sdk.AutoLogAppEventsEnabled"
        android:value="true" />
    <meta-data
        android:name="com.facebook.sdk.AutoLogMetaDataEnabled"
        android:value="false" />
</application>
Enter fullscreen mode Exit fullscreen mode

With valid manifest metadata, Meta's Android library initializes automatically. With automatic App Events enabled, it also logs app activation; I did not add a second manual activateApp() call. The SDK's own source documents this behavior. Also check the automatic App Events setting in Meta Events Manager, because Meta says both the client setting and the Events Manager setting affect automatic logging.

This project also sets FacebookSdk.setGraphApiVersion("v24.0") before creating its logger because the tested SDK version otherwise used an older Graph API version. That is a project-specific compatibility setting. Recheck it when upgrading the SDK or when Meta changes supported API versions.

Step 3: Decide exactly what each event means

Tracking is only useful if an event fires at a meaningful point. I mapped the booking flow before writing the Flutter calls:

Event Type When it fires What it tells me
fb_mobile_activate_app Automatic SDK observes an app session The app was opened; this is not a ticket sale
travler_home_view Custom Booking home becomes visible Entry into the booking flow
travler_search_screen_view Custom Search/results screen opens User reached the search stage
fb_mobile_search Standard Search User submits a route search Intent to find a trip
travler_search_results_view Custom Results load, with result_count Search succeeded, including zero results
travler_search_results_error Custom Results request fails A technical drop-off point
fb_mobile_initiated_checkout Standard Initiated Checkout Payments page opens with a valid amount User reached checkout
fb_mobile_purchase Standard Purchase Payment is confirmed successful A paid ticket conversion

The standard events use Meta's event constants or its logPurchase API. The custom names are specific to this app. A Purchase must not fire when someone merely taps Pay, opens a pending booking, or sees a payment request. It fires after the payment-success path confirms the booking was paid.

For accurate interpretation, do not treat raw event totals as a perfect person-by-person funnel. One user can search several times, return to checkout, or reopen the app. Use the counts to locate likely drop-offs, then compare unique users or sessions in a suitable analytics report before calculating conversion rates.

Step 4: Bridge Flutter actions to native App Events

I used a Flutter MethodChannel because this project was adding Android tracking first. The Dart side invokes a named action; MainActivity maps that action to Meta's Android logger. Both sides must use the same channel name and method names.

A reduced Dart example:

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class MetaEvents {
  static const _channel = MethodChannel('com.ma3app/meta_app_events');

  static bool get _android =>
      !kIsWeb && defaultTargetPlatform == TargetPlatform.android;

  static Future<void> searchSubmitted() async {
    if (_android) await _channel.invokeMethod('logSearchSubmitted');
  }

  static Future<void> purchase({
    required String bookingId,
    required double amount,
    required String currency,
  }) async {
    if (!_android || amount <= 0) return;
    await _channel.invokeMethod('logPurchase', {
      'bookingId': bookingId,
      'amount': amount,
      'currency': currency,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The native handler creates an AppEventsLogger and maps these calls to standard or custom events. The relevant Android calls look like this:

val events = AppEventsLogger.newLogger(applicationContext)

// Search submitted:
events.logEvent(AppEventsConstants.EVENT_NAME_SEARCHED)

// Checkout started with a valid positive amount:
events.logEvent(
    AppEventsConstants.EVENT_NAME_INITIATED_CHECKOUT,
    amount,
    Bundle().apply {
        putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currencyCode)
    },
)

// Payment confirmed with a valid positive amount:
events.logPurchase(
    BigDecimal.valueOf(amount),
    Currency.getInstance(currencyCode),
)
Enter fullscreen mode Exit fullscreen mode

The actual channel handler validates numeric amounts and ISO currency codes, returns Flutter errors for invalid arguments, and logs the extra screen/result events. It also keeps purchase deduplication keyed by booking reference on the device, so two success callbacks do not log two purchases for one booking. Event delivery is asynchronous: a successful method call means the native logger accepted the event, while the SDK may flush it to Meta later.

Only pass parameters needed for measurement. Do not send names, phone numbers, payment credentials, access tokens, or raw logs as event parameters. If a booking reference can identify a customer, keep it locally for deduplication unless there is a reviewed reason to transmit it.

Step 5: Connect the registered app to campaigns

In Meta Business settings, find the registered mobile app and make it available to the ad account that will run campaigns. In Ads Manager, create an app campaign, select the Android app, and confirm that the selected App ID matches the SDK App ID. Select an available performance goal/event appropriate to the campaign; event optimization options can depend on the app's eligibility and event volume.

For install campaigns, the app and its store listing must be correct. For ticket-sale campaigns, the app must actually send paid Purchase events. A Pixel ID is relevant if a separate campaign sends people to the website and measures purchases there; it does not replace the native app's App ID.

Step 6: Test in three layers

A. Confirm local SDK initialization

Run a build that contains the integration. In a debug build, I log four booleans without printing the token itself:

initialized=true autoLog=true appIdMatches=true clientTokenPresent=true
Enter fullscreen mode Exit fullscreen mode

appIdMatches compares the SDK's loaded ID to the Android resource. It does not prove that I selected the correct app in Ads Manager.

B. Confirm an event is created and sent

Open the app and use Android logcat. One focused command is:

adb logcat -s \
  TravlerMetaSDK:I \
  FacebookSDK.AppEvents:D \
  FacebookSDK.com.facebook.appevents.AppEventQueue:D
Enter fullscreen mode Exit fullscreen mode

For my Android test build, the logs showed fb_mobile_activate_app, then Result: Success after an App Events flush. They also showed travler_home_view and another successful flush. That is strong evidence the SDK initialized and reported successful delivery. It does not yet prove that a Purchase event was sent or that any campaign received credit.

Logcat can include device IDs and other telemetry. Extract only the event names and send results when sharing a diagnostic log.

C. Confirm the right event appears in Meta

In Events Manager, select the app data source for the same App ID, not the website Pixel. Check Test events or the app's event overview for Activate App, Search, Initiated Checkout, and Purchase as you trigger each stage. For Purchase, complete a legitimate test payment on a build with the integration and verify the amount and currency.

Finally, test a campaign that selects that same app. Organic or debug events may appear in Events Manager without showing as ad-attributed conversions in Ads Manager. Attribution requires an eligible ad interaction and follows Meta's reporting rules. A local Result: Success is a delivery check, not a campaign ROI result.

Troubleshooting checklist

  • Ad Account ID and App ID differ: Expected. Do not copy the Ad Account ID into Android resources.
  • Pixel ID and App ID differ: Expected. Check the app data source for native app events.
  • Campaign selects a different App ID: Give the ad account access to the correct app and select it, or reconfigure the SDK with the intended app's App ID and matching Client Token and ship a new build. Events already sent to the old app do not move automatically.
  • No Activate App locally: Check that the installed build has the SDK, the manifest has the correct values, and automatic App Events are enabled. Do not add a duplicate manual activation call without first checking the SDK configuration.
  • Activate App appears, but Purchase does not: Check the actual payment-success callbacks, amount validation, and booking deduplication. Opening the payments page only tests Initiated Checkout.
  • Events Manager has Purchase, but Ads Manager has no conversions: Check the campaign's selected app, business access, release build, ad interaction, and attribution settings. A test purchase with no Meta ad interaction is not an ad-attributed purchase.
  • Debug works, production does not: Check the release signing certificate/key hash, Play distribution, package name, app version, and build-time Client Token. Test the distributed release on a physical device.

The rule I keep returning to is simple: the Android SDK and the app selected in the Meta campaign must refer to the same Meta App ID. The Ad Account ID identifies who runs the ads; the Pixel ID is for website measurement.

References

Top comments (0)