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:
_10flutter 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:
_10import '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:
_11void main() {_11 runApp(const MyApp());_11}_11_11class 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:
_10MyApp(_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:
_12BottomNavigationBar(_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)
Tracking deep links
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:
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.
-
Register a custom URL scheme in
Info.plist: -
Handle incoming URLs in
AppDelegate.swift(orAppDelegate.mm) for both cold and warm starts:_37import Flutter_37import UIKit_37import activity_flow_37_37@main_37@objc class AppDelegate: FlutterAppDelegate {_37override func application(_37_ application: UIApplication,_37didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?_37) -> Bool {_37GeneratedPluginRegistrant.register(with: self)_37_37// Capture initial URL if app was launched with a deep link (cold start)_37if let initialURL = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {_37DeepLinkManager.shared.handle(url: initialURL)_37}_37_37return super.application(application, didFinishLaunchingWithOptions: launchOptions)_37}_37_37// Handles incoming URLs from Custom URL Schemes (e.g., myapp://path)_37override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {_37DeepLinkManager.shared.handle(url: url)_37return super.application(app, open: url, options: options)_37}_37_37// Handles incoming URLs from Universal Links_37override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {_37// Check if the activity is a web browsing activity (Universal Link)_37if userActivity.activityType == NSUserActivityTypeBrowsingWeb {_37if let url = userActivity.webpageURL {_37DeepLinkManager.shared.handle(url: url)_37}_37}_37return super.application(application, continue: userActivity, restorationHandler: restorationHandler)_37}_37} -
Configure
SceneDelegate
If your project uses a SceneDelegate, also forward deep links there:
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:
- Call the
addAdsListenermethod
To enable tracking, call the addAdsListener extension method on any Flutter widget that represents an ad:
_10yourAdWidget.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:
_48import 'package:activity_flow/activity_flow.dart';_48import 'package:flutter/material.dart';_48_48class 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
elementSourcekey in the method's metadata map.
_10ElevatedButton(_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
elementSourcekey in the method's metadata map.
_10ProductCard(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
elementSourcekey in the method's metadata map.
_10ProductCard(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:
_10ProductCard(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:
-
In your terminal, run
flutter test --dart-define=ACTIVITY_FLOW_TEST_ENV=true. -
In a code editor, open your project.
-
In the code editor settings, search for
dart.flutterTestAdditionalArgs. -
Add to it the value
--dart-define=ACTIVITY_FLOW_TEST_ENV=true.Alternatively, open the
settings.jsonfile 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:
_99import 'package:activity_flow/activity_flow.dart';_99import 'package:flutter/material.dart';_99import 'package:example/screens/favorite.dart';_99import 'package:example/screens/products.dart';_99import 'package:example/screens/profile.dart';_99_99void 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._99class 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._99class 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._99class 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.