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
),
);
Query Params from Deep Links
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
To start collecting ad events (impression, view, and click), use the extension addAdsListener at the end of any widget that is considered an ad widget. This extension receives a map of data params related to the ad. The adId params is mandatory.
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 theACTIVITY_FLOW_TEST_ENVparameter when running tests with the test button. To fix this, add--dart-define=ACTIVITY_FLOW_TEST_ENV=trueto your VS Code settings. Search fordart.flutterTestAdditionalArgsin the settings and add the value--dart-define=ACTIVITY_FLOW_TEST_ENV=true. Alternatively, edit yoursettings.jsonfile and include:"dart.flutterTestAdditionalArgs": ["--dart-define=ACTIVITY_FLOW_TEST_ENV=true"] -
Where should I call
initActivityFlow? Always callinitActivityFlowinside thebuildmethod of your main widget (e.g.,MyApp). This ensures Activity Flow is disabled every time you usepumpWidget(const MyApp()); otherwise, it may interfere with the app tests.
Libraries
- activity_flow
- This file provides all the events.