Documentation
Feedback
Guides
API Reference

Guides
Guides

Installing Activity Flow in Flutter apps

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 from 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:


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

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 like BottomNavigationBar or TabBar, which do not trigger route changes, use the trackPageView function to track screen views manually.

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 `trackPageView` with the label
_12
trackPageView(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:


_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
_17
android:scheme="https"
_17
android:host="example.com"
_17
android:pathPrefix="{APP_ROUTE}" />
_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:

_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>

  1. 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
}

  1. 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
}

(Optional) 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:

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

To start tracking your ad events, follow these steps:

  1. Call the addAdsListener

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 manual tracking for tab changes:


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

This Flutter screen, 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.

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 lets you pass compile-time key=value pairs into your Flutter app as Dart environment declarations. 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.
  5. Open the settings.json file of your project.
  6. Add the following: "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:


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

The example demonstrates the integration of 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, which utilizes 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)
Contributors
1
Photo of the contributor
Was this helpful?
Suggest edits (GitHub)
On this page