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:
_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:
_13void main() {_13runApp(const MyApp());_13}_13_13class 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:
_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 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:
_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 `trackPageView` with the label_12 trackPageView(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:
_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.
- 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>
- 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 {_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}
- Configure
SceneDelegate
If your project uses a SceneDelegate, also forward deep links there:
(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:
- Call the
addAdsListener
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 manual tracking for tab changes:
_47import 'package:activity_flow/activity_flow.dart';_47import 'package:flutter/material.dart';_47_47class 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:
- 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. - Open the
settings.jsonfile of your project. - 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:
_93import 'package:activity_flow/activity_flow.dart';_93import 'package:flutter/material.dart';_93import 'package:example/screens/favorite.dart';_93import 'package:example/screens/products.dart';_93import 'package:example/screens/profile.dart';_93_93void 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._93class 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._93class 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._93class 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.