VTEX Activity Flow Mobile RUM for Flutter apps focused in Android and iOS.

Features

This plugin coupled to an app, aims to:

  • Track user navigation between app pages and send events.

Usage

1. Import the package inside the main.dart file:

import 'package:activity_flow/activity_flow.dart';

2. Initialize Activity Flow at app startup

Call initActivityFlow inside the build method of your main widget (e.g., MyApp), passing your account name (and any extra params if needed), as in the example:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    initActivityFlow(accountName: appAccountName);

    return MaterialApp(
      // rest of the code

Screen view event

Create an instance of the Screen View observer class, by setting the account name:


void main() {
  runApp(
    ...
      final afObserver = PageViewObserver();
    ...
  )
}

Automatically tracks your route navigation using the previous AF observer instance created:

MaterialApp(
  // Add the routeObserver to the navigatorObservers list.
  navigatorObservers: [ afObserver ],
  routes: {
    ... //define your named routes
  },
);

Manually Track Navigation Using trackPageView

Since NavigatorObserver currently lacks support for automatically tracking screen view events in Bottom Navigation or Tab Navigation, trackPageView can be effectively utilized for manual event tracking.

// Example for BottomNavigationBar
BottomNavigationBar(
  items: items,
  currentIndex: _selectedIndex,
  onTap: (index) {
    _onItemTapped(index);
    final label = items[index].label ?? 'Tab-$index';
    trackPageView(label);
  },
)

Touch event

Automatically tracks touch gesture using the detector as a Provider in your app, together with the page view observer to get routes names too.

    return TouchDetector( //AF config
      accountName: {accountName},
      child: MaterialApp(
        ...
        navigatorObservers: [afObserver], //AF config
      ),
    );

The Activity Flow SDK now automatically captures query parameters from deep links and includes them in page view events. You need to configure the deep links in your app, depending on the platform. For Android check the Android Setup section. For iOS check the iOS Setup section.

Android Setup

To enable deep link handling on Android, you need to add intent filters to your AndroidManifest.xml file. Each route that can be accessed via deep link requires its own intent filter.

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

Add intent filters inside the <activity> tag for your main activity:

<!-- Example for HTTPS deep links -->
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="https" android:host="{YOUR_DOMAIN}" android:pathPrefix="{YOUR_APP_ROUTE}" />
</intent-filter>

<!-- Example for custom URL scheme -->
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="{YOUR_CUSTOM_SCHEME}" />
</intent-filter>

Important: For each route in your app that should be accessible via deep link, add a new intent filter. The main difference between intent filters is the android:pathPrefix attribute, which specifies the route path.

Example with multiple routes:

<!-- Route for product pages -->
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="https" android:host="mystore.com" android:pathPrefix="/product" />
</intent-filter>

<!-- Route for checkout -->
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="https" android:host="mystore.com" android:pathPrefix="/checkout" />
</intent-filter>

iOS Setup

iOS deep link configuration requires changes in two files:

Step 1: Configure Info.plist

Add the following configuration to your ios/Runner/Info.plist file to register your custom URL scheme:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>{YOUR_BUNDLE_URL_SCHEME}</string>
    </array>
    <key>CFBundleURLName</key>
    <string>{YOUR_BUNDLE_URL_NAME}</string>
  </dict>
</array>

Example:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
    <key>CFBundleURLName</key>
    <string>com.mycompany.myapp</string>
  </dict>
</array>

Step 2: Configure AppDelegate and SceneDelegate

Modify your ios/Runner/AppDelegate.swift and create/modify ios/Runner/SceneDelegate.swift to handle incoming deep links.

Important: The following code handles deep links for both cold starts (when the app is not running) and warm starts (when the app is already running).

AppDelegate.swift:

Replace the contents of ios/Runner/AppDelegate.swift with:

import Flutter
import UIKit
import activity_flow

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    
    // Capture initial URL if app was launched with a deep link (cold start)
    if let initialURL = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {
      DeepLinkManager.shared.handle(url: initialURL)
    }
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  /**
   * Handles incoming URLs from Custom URL Schemes (e.g., myapp://path)
   * This is called when the app is opened via a custom URL scheme.
   */
  override func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey : Any] = [:]
  ) -> Bool {
    // Handle deep link with DeepLinkManager
    DeepLinkManager.shared.handle(url: url)
    
    // Pass to Flutter
    return super.application(app, open: url, options: options)
  }
  
  /**
   * Handles incoming URLs from Universal Links (e.g., https://mystore.com/product)
   * Universal Links allow your app to be opened from standard HTTPS URLs.
   */
  override func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
  ) -> Bool {
    // Check if the activity is a web browsing activity (Universal Link)
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
      if let url = userActivity.webpageURL {
        DeepLinkManager.shared.handle(url: url)
      }
    }
    
    return super.application(
      application,
      continue: userActivity,
      restorationHandler: restorationHandler
    )
  }
}

SceneDelegate.swift:

Create or modify ios/Runner/SceneDelegate.swift with:

import UIKit
import Flutter
import activity_flow

class SceneDelegate: FlutterSceneDelegate {
  
  override func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    // Capture deep links on cold start
    var hasDeepLink = false
    
    // Check for Universal Links
    if let userActivity = connectionOptions.userActivities.first(where: {
      $0.activityType == NSUserActivityTypeBrowsingWeb
    }) {
      if let url = userActivity.webpageURL {
        DeepLinkManager.shared.handle(url: url)
        hasDeepLink = true
      }
    }
    // Check for Custom URL Schemes
    else if let url = connectionOptions.urlContexts.first?.url {
      DeepLinkManager.shared.handle(url: url)
      hasDeepLink = true
    }
    
    // Call super with empty options if a deep link was handled
    if hasDeepLink {
      super.scene(scene, willConnectTo: session, options: UIScene.ConnectionOptions())
    } else {
      super.scene(scene, willConnectTo: session, options: connectionOptions)
    }
  }
  
  /**
   * Handles incoming URLs from Custom URL Schemes when app is already running
   */
  override func scene(
    _ scene: UIScene,
    openURLContexts URLContexts: Set<UIOpenURLContext>
  ) {
    if let url = URLContexts.first?.url {
      DeepLinkManager.shared.handle(url: url)
    }
  }

  /**
   * Handles incoming Universal Links when app is already running
   */
  override func scene(
    _ scene: UIScene,
    continue userActivity: NSUserActivity
  ) {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
      if let url = userActivity.webpageURL {
        DeepLinkManager.shared.handle(url: url)
      }
    }
  }
}

Note: Make sure to import activity_flow at the top of both files to access DeepLinkManager

The query parameters are automatically captured and included in the page view events, providing better tracking and analytics for deep link attribution.

Ad Events

return AnyAdWidget(

  // Widget code ...

).addAdsListener({
  "adId": "ad-123",
  "foo": "bar",
  "abc": "xyz",
});

Test Environment Setup

Why enable the test environment variable?

By enabling the test environment variable, you disable Activity Flow to run your app tests without interference. This is necessary because the plugin and its dependencies have some timers defined that can be unpredictable in test environments.

Enabling the test environment variable will:

  • Prevent side effects or asynchronous behaviors that could interfere with test assertions.
  • Ensure your analytics and event tracking logic is test-friendly and deterministic.

How to enable the test environment variable

Add the following Dart define when running your tests:

flutter test --dart-define=ACTIVITY_FLOW_TEST_ENV=true

FAQ

  • Why do my tests pass with flutter test --dart-define=ACTIVITY_FLOW_TEST_ENV=true, but fail when using the test button in VS Code? VS Code does not automatically add the ACTIVITY_FLOW_TEST_ENV parameter when running tests with the test button. To fix this, add --dart-define=ACTIVITY_FLOW_TEST_ENV=true to your VS Code settings. Search for dart.flutterTestAdditionalArgs in the settings and add the value --dart-define=ACTIVITY_FLOW_TEST_ENV=true. Alternatively, edit your settings.json file and include:

    "dart.flutterTestAdditionalArgs": ["--dart-define=ACTIVITY_FLOW_TEST_ENV=true"]
    
  • Where should I call initActivityFlow? Always call initActivityFlow inside the build method of your main widget (e.g., MyApp). This ensures Activity Flow is disabled every time you use pumpWidget(const MyApp()); otherwise, it may interfere with the app tests.

Libraries

activity_flow
This file provides all the events.