Documentation
Feedback
Guides
API Reference

Guides
Guides

Installing Activity Flow in Flutter apps

Learn how to install the VTEX Activity Flow SDK in your Flutter app to track navigation, page views, deep links, ads, and customer interactions across Android and iOS.

In this guide, you'll learn how to install and configure the VTEX Activity Flow SDK in Flutter apps for Android and iOS. By following these steps, you'll be able to track user navigation, handle deep links, and collect ad events and customer interaction events (clicks, views, and impressions) in your app.

Before you begin

Install the Activity Flow package

The Activity Flow SDK captures user navigation and sends events from your mobile app. To install the package, run the following command in your project directory:


_10
flutter pub add activity_flow

This installs the SDK and updates your pubspec.yaml file with the activity_flow dependency.

Instructions

Step 1 – Importing the Activity Flow package

In your app's main file, import the Activity Flow package as follows:


_10
import 'package:activity_flow/activity_flow.dart';

Step 2 – Creating an Activity Flow instance

Set the account name to create an instance of the main package class:


_11
void main() {
_11
runApp(const MyApp());
_11
}
_11
_11
class App extends StatelessWidget {
_11
Widget build(BuildContext context) {
_11
// Call activity flow here
_11
initActivityFlow(accountName: appAccountName);
_11
...
_11
}
_11
}

Usage

Tracking page views automatically

To automatically track user navigation between app pages, add the PageViewObserver to the navigatorObservers list in your app:


_10
MyApp(
_10
// Add the PageViewObserver to the navigatorObservers list.
_10
navigatorObservers: [PageViewObserver()],
_10
routes: {
_10
// Define your named routes here
_10
},
_10
_10
),

This setup enables automatic screen view tracking for standard route navigation.

Tracking page views manually (for custom navigation)

For navigation widgets such as BottomNavigationBar or TabBar that don't trigger route changes, use the screenViewChange function to manually track screen views.

For example, using the onTap callback within a BottomNavigationBar widget allows for capturing a new route each time the user taps on a different tab:


_12
BottomNavigationBar(
_12
items: items,
_12
currentIndex: _selectedIndex,
_12
selectedItemColor: Colors.pink,
_12
onTap: (index) {
_12
_onItemTapped(index);
_12
final label = items[index].label ?? 'Tab-$index';
_12
_12
// Manually calling the `screenViewChange` with the label
_12
screenViewChange(label);
_12
},
_12
)

The Activity Flow SDK automatically captures deep link query parameters and includes them in page view events when your app is configured for deep linking. Configure each platform as follows.

Android

Add intent filters to AndroidManifest.xml for each route that can be accessed via deep link:

android/app/src/main/AndroidManifest.xml

_17
<intent-filter>
_17
<action android:name="android.intent.action.VIEW" />
_17
<category android:name="android.intent.category.DEFAULT" />
_17
<category android:name="android.intent.category.BROWSABLE" />
_17
<data
_17
android:scheme="https"
_17
android:host="example.com"
_17
android:pathPrefix="{APP_ROUTE}"
_17
/>
_17
</intent-filter>
_17
_17
<intent-filter>
_17
<action android:name="android.intent.action.VIEW" />
_17
<category android:name="android.intent.category.DEFAULT" />
_17
<category android:name="android.intent.category.BROWSABLE" />
_17
<data android:scheme="YOUR_CUSTOM_SCHEME" />
_17
</intent-filter>

The main difference between intent filters for different routes is the android:pathPrefix attribute, which specifies the app route.

iOS

Configure both Info.plist and the app delegate to handle deep links.

  1. Register a custom URL scheme in Info.plist:

    ios/{YourApp}/Info.plist

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

  2. Handle incoming URLs in AppDelegate.swift (or AppDelegate.mm) for both cold and warm starts:


    _37
    import Flutter
    _37
    import UIKit
    _37
    import activity_flow
    _37
    _37
    @main
    _37
    @objc class AppDelegate: FlutterAppDelegate {
    _37
    override func application(
    _37
    _ application: UIApplication,
    _37
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    _37
    ) -> Bool {
    _37
    GeneratedPluginRegistrant.register(with: self)
    _37
    _37
    // Capture initial URL if app was launched with a deep link (cold start)
    _37
    if let initialURL = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {
    _37
    DeepLinkManager.shared.handle(url: initialURL)
    _37
    }
    _37
    _37
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    _37
    }
    _37
    _37
    // Handles incoming URLs from Custom URL Schemes (e.g., myapp://path)
    _37
    override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    _37
    DeepLinkManager.shared.handle(url: url)
    _37
    return super.application(app, open: url, options: options)
    _37
    }
    _37
    _37
    // Handles incoming URLs from Universal Links
    _37
    override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    _37
    // Check if the activity is a web browsing activity (Universal Link)
    _37
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
    _37
    if let url = userActivity.webpageURL {
    _37
    DeepLinkManager.shared.handle(url: url)
    _37
    }
    _37
    }
    _37
    return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
    _37
    }
    _37
    }

  3. Configure SceneDelegate

If your project uses a SceneDelegate, also forward deep links there:

ios/Runner/SceneDelegate.swift

_41
import UIKit
_41
import Flutter
_41
import activity_flow
_41
_41
class SceneDelegate: FlutterSceneDelegate {
_41
override func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
_41
// Capture deep links on cold start
_41
var hasDeepLink = false
_41
_41
if let userActivity = connectionOptions.userActivities.first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb }) {
_41
if let url = userActivity.webpageURL {
_41
DeepLinkManager.shared.handle(url: url)
_41
hasDeepLink = true
_41
}
_41
} else if let url = connectionOptions.urlContexts.first?.url {
_41
DeepLinkManager.shared.handle(url: url)
_41
hasDeepLink = true
_41
}
_41
_41
// Call super with empty options if a deep link was handled
_41
if hasDeepLink {
_41
super.scene(scene, willConnectTo: session, options: UIScene.ConnectionOptions())
_41
} else {
_41
super.scene(scene, willConnectTo: session, options: connectionOptions)
_41
}
_41
}
_41
_41
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
_41
if let url = URLContexts.first?.url {
_41
DeepLinkManager.shared.handle(url: url)
_41
}
_41
}
_41
_41
override func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
_41
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
_41
if let url = userActivity.webpageURL {
_41
DeepLinkManager.shared.handle(url: url)
_41
}
_41
}
_41
}
_41
}

Tracking ad events

Ads tracking is available only for accounts using VTEX Ads. If you're interested in this feature, open a ticket with VTEX Support.

When you apply the listener to a widget, the SDK automatically tracks three events:

  • Impressions: When the widget is first built and rendered.
  • Views: When at least 50% of the widget is visible on screen for at least 1 continuous second.
  • Clicks: When the user taps the widget.

To start tracking your ad events, follow these steps:

  1. Call the addAdsListener method

To enable tracking, call the addAdsListener extension method on any Flutter widget that represents an ad:


_10
yourAdWidget.addAdsListener(Map<String, String> adMetadata);

  • yourAdWidget: The ad widget you want to monitor.
  • adMetadata: A map containing specific details about the ad, which will be sent to your analytics service upon a click.

The following example initializes the Activity Flow to track page views automatically and demonstrates ad tracking:


_48
import 'package:activity_flow/activity_flow.dart';
_48
import 'package:flutter/material.dart';
_48
_48
class HomeScreen extends StatelessWidget {
_48
const HomeScreen({super.key});
_48
_48
@override
_48
Widget build(BuildContext context) {
_48
return Scaffold(
_48
appBar: AppBar(title: const Text('Home')),
_48
body: ListView(
_48
children: [
_48
const Padding(
_48
padding: EdgeInsets.all(16.0),
_48
child: Text('Featured Products'),
_48
),
_48
_48
// Standard content
_48
const ProductTile(name: 'Product A'),
_48
const ProductTile(name: 'Product B'),
_48
_48
// Ad Widget with Activity Flow tracking
_48
// We wrap the visual component (e.g., Image, Container) with the listener
_48
Padding(
_48
padding: const EdgeInsets.symmetric(vertical: 10.0),
_48
child: Container(
_48
height: 150,
_48
width: double.infinity,
_48
color: Colors.grey[200],
_48
child: Image.asset(
_48
'assets/promo_banner.jpg',
_48
fit: BoxFit.cover,
_48
),
_48
).addAdsListener({
_48
'adId': 'summer_campaign_123',
_48
'creativeId': 'banner_v1',
_48
'position': 'list_middle',
_48
'campaignName': 'Summer Sale 2024',
_48
}),
_48
),
_48
_48
// More content
_48
const ProductTile(name: 'Product C'),
_48
],
_48
),
_48
);
_48
}
_48
}

This Flutter screen is constructed as a StatelessWidget that lists products and displays an ad banner. The banner's Container is wrapped with Activity Flow's addAdsListener, which attaches an ad-event listener and sends the provided metadata map with each event.

This instrumentation enables the automatic tracking of impressions, viewability, and clicks, allowing for comprehensive analytics tied to adId, creativeId, position, and campaignName.

Tracking customer interaction events

Beyond page views and ad events, Activity Flow provides individual extension methods for tracking click, view, and impression events on any widget. These methods can be used independently or combined on the same widget.

All methods require elementSource, a string that identifies the tracked element. This field is sent as a top-level property in the event payload, separate from the other attributes.

Click event

Use addClickListener on any widget to capture tap interactions. A click event is fired when the user taps (not drags) on the element. Pass any custom attributes you want to associate with the interaction.

You must include the elementSource key in the method's metadata map.


_10
ElevatedButton(
_10
onPressed: () => Navigator.pushNamed(context, '/checkout'),
_10
child: const Text('Buy Now'),
_10
).addClickListener({
_10
'elementSource': 'buy-now-button',
_10
'productId': product.id,
_10
});

View event

Use addViewListener to fire a view event when the widget has been at least 50% visible on screen for at least 1 second (IAB standard). The event fires once per app session — it does not re-fire if the widget scrolls off-screen and back, or if the user navigates away and returns.

You must include the elementSource key in the method's metadata map.


_10
ProductCard(product: product).addViewListener({
_10
'elementSource': 'product-card',
_10
'productId': product.id,
_10
});

Impression events

Use the addImpressionListener method to trigger an impression event immediately upon the widget being built and rendered within the application's widget tree.

You must include the elementSource key in the method's metadata map.


_10
ProductCard(product: product).addImpressionListener({
_10
'elementSource': 'product-card',
_10
'productId': product.id,
_10
});

Combining multiple listeners

Combine the listeners on a single widget to track the full interaction funnel: whether the element was rendered (impression), actually seen by the user (view), and clicked (click).

The example below chains the impression and view listeners on a single widget to capture both event types simultaneously:


_10
ProductCard(product: product)
_10
.addImpressionListener({
_10
'elementSource': 'product-card',
_10
'productId': product.id,
_10
})
_10
.addViewListener({
_10
'elementSource': 'product-card',
_10
'productId': product.id,
_10
});

Flutter automated tests

To ensure your Flutter test runs work correctly when the Activity Flow is installed, run tests with a test environment flag using a --dart-define flag.

The --dart-define flag allows you to pass compile-time key=value pairs into your Flutter app as Dart environment declarations. To do so, follow the steps below:

  1. In your terminal, run flutter test --dart-define=ACTIVITY_FLOW_TEST_ENV=true.

  2. In a code editor, open your project.

  3. In the code editor settings, search for dart.flutterTestAdditionalArgs.

  4. Add to it the value --dart-define=ACTIVITY_FLOW_TEST_ENV=true.

    Alternatively, open the settings.json file of your project and add "dart.flutterTestAdditionalArgs": ["--dart-define=ACTIVITY_FLOW_TEST_ENV=true"].

Use case example

Below is an example that contains an app with some pages and navigation through them:


_99
import 'package:activity_flow/activity_flow.dart';
_99
import 'package:flutter/material.dart';
_99
import 'package:example/screens/favorite.dart';
_99
import 'package:example/screens/products.dart';
_99
import 'package:example/screens/profile.dart';
_99
_99
void main() {
_99
runApp(const ExampleApp());
_99
}
_99
_99
/// A MaterialApp with a custom theme and routes.
_99
///
_99
/// The routes are defined in the [routes] property.
_99
/// The theme is defined in the [theme] property.
_99
class ExampleApp extends StatelessWidget {
_99
const ExampleApp({super.key});
_99
_99
@override
_99
Widget build(BuildContext context) {
_99
initActivityFlow(accountName: appAccountName);
_99
_99
return MaterialApp(
_99
title: 'Example App',
_99
theme: ThemeData(
_99
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
_99
useMaterial3: true,
_99
),
_99
routes: {
_99
'/': (context) => const MyHomePage(),
_99
'/products': (context) => const ProductsScreen(),
_99
'/profile': (context) => const ProfileScreen(),
_99
'/favorites': (context) => const FavoriteScreen(),
_99
},
_99
initialRoute: '/',
_99
navigatorObservers: [PageViewObserver()],
_99
);
_99
}
_99
}
_99
_99
/// A home screen with buttons to navigate to other screens.
_99
class MyHomePage extends StatelessWidget {
_99
const MyHomePage({super.key});
_99
_99
final List<Map> _routes = const [
_99
{
_99
'name': 'Products',
_99
'route': '/products',
_99
},
_99
{
_99
'name': 'Profile',
_99
'route': '/profile',
_99
}
_99
];
_99
_99
@override
_99
Widget build(BuildContext context) {
_99
return Scaffold(
_99
appBar: AppBar(
_99
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
_99
title: const Text('Home Screen'),
_99
),
_99
body: Center(
_99
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
_99
const AdBanner().addAdsListener({
_99
'productName': 'Sneakers',
_99
'productPrice': '59.99',
_99
'adID': '1123',
_99
}),
_99
..._routes.map((route) => ButtonTemplate(
_99
title: route['name'],
_99
route: route['route'],
_99
)),
_99
])),
_99
);
_99
}
_99
}
_99
_99
/// A template for creating buttons.
_99
///
_99
/// Receives a [title], [icon], and [route] to navigate to.
_99
/// Returns an [ElevatedButton.icon] with the given parameters.
_99
class ButtonTemplate extends StatelessWidget {
_99
const ButtonTemplate({
_99
super.key,
_99
required this.title,
_99
required this.route,
_99
});
_99
_99
final String title;
_99
final String route;
_99
_99
@override
_99
Widget build(BuildContext context) {
_99
return ElevatedButton(
_99
onPressed: () => Navigator.pushNamed(context, route),
_99
child: Text(title),
_99
);
_99
}
_99
}

The example demonstrates how to integrate Activity Flow into a Flutter app by importing the necessary package, initializing it with initActivityFlow(accountName: appAccountName), and constructing a MaterialApp with named routes and a PageViewObserver to automatically capture page-view events.

It outlines a MyHomePage that incorporates an AdBanner that uses addAdsListener to pass ad metadata, such as product name, price, and ID. Additionally, it features navigation buttons sourced from a routes list.

The reusable ButtonTemplate facilitates navigation through Navigator.pushNamed, showcasing a standard configuration for automatic screen tracking, as well as ad impression and click tracking, in a Flutter application.

Contributors
1
Photo of the contributor
Was this helpful?
Yes
No
Suggest Edits (GitHub)
See also
VTEX Activity Flow
Guides
Contributors
1
Photo of the contributor
Was this helpful?
Suggest edits (GitHub)
On this page