Skip to main content
Configure how your app handles navigation, analytics events, and experience lifecycle callbacks. Triggered when a deep link is invoked from an experience or push notification. Parameters:
  • url: String – The custom deep link URL that should be handled by the host (client) app.

Analytics Listener

Triggered when the client app reports an analytics event to the SDK. Parameters:
  • analytic: String – The name or type of the analytic event ("Identify", "Screen", "Event").
  • value: String – The value associated with the event.
  • properties: Map<String, Any> – Additional key-value pairs providing context for the event.

Experience Listener

Provides callbacks related to the lifecycle of experiences and their steps within the SDK. Callbacks onExperienceStateChanged(id: Int, state: String) Called when the overall state of an experience changes.
  • id: Experience ID (optional)
  • state: New state — "Started", "Completed", "Dismissed", "Skipped", or "Submitted"
  • experienceType: "Flow", "Survey", or "NPS"
onExperienceStepStateChanged(id: Int, state: String, experienceId: Int, step: Int, totalSteps: Int) Called when the state of a specific step in an experience changes.
  • id: Step ID
  • state: New step state — "Started", "Completed", "Dismissed", "Skipped", or "Submitted"
  • experienceId: Associated experience ID
  • experienceType: "Flow", "Survey", or "NPS"
  • step: Current step index (optional)
  • totalSteps: Total number of steps in the experience (optional)
Note: Both onExperienceStateChanged and onExperienceStepStateChanged events are sent under the key UserpilotExperienceEvent.

Implementation Example

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:userpilot_flutter/userpilot_flutter.dart';
import 'package:userpilot_flutter_example/src/utils/routes.dart';
import '../utils/app_colors.dart';

class UserpilotApp extends StatefulWidget {
  const UserpilotApp({super.key});

  @override
  State<UserpilotApp> createState() => _UserpilotAppState();
}

class _UserpilotAppState extends State<UserpilotApp> {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  StreamSubscription<UserpilotExperience>? experienceSubscription;
  StreamSubscription<UserpilotAnalytic>? analyticsSubscription;
  StreamSubscription<UserpilotNavigation>? navigationSubscription;

  // Event Channel creation
  // used for listening for deep links from platform code
  static const stream = EventChannel('com.userpilot.samples.flutter/events');

  // Method channel creation
  // used to check for initial deep link when launching app, from platform
  static const platform =
      MethodChannel('com.userpilot.samples.flutter/channel');

  bool _initialURILinkHandled = false;

  @override
  void initState() {
    super.initState();
    _initializeUserpilot();

    //Checking application start by deep link
    _startUri().then(_onRedirected);
    //Checking broadcast stream, if deep link was clicked in opened application
    stream.receiveBroadcastStream().listen((d) => _onRedirected(d));
  }

  @override
  void dispose() {
    experienceSubscription?.cancel();
    analyticsSubscription?.cancel();
    navigationSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
    );
  }

  Future<void> _initializeUserpilot() async {
    UserpilotOptions options = UserpilotOptions();
    options.logging = true;

    try {
      await Userpilot.initialize('APP_TOKEN', options);
      print('Userpilot initialized successfully');

      // Subscribe to streams AFTER initialization completes
      // This ensures the native StreamHandlers are set up before subscribing
      _subscribeToUserpilotStreams();

      await _printUserpilotSettings();
    } catch (e) {
      print('Failed to initialize Userpilot: $e');
    }
  }

  void _subscribeToUserpilotStreams() {
    experienceSubscription = Userpilot.onExperienceEvent.listen(
      _handleExperienceEvent,
      onError: _handleError,
    );

    analyticsSubscription = Userpilot.onAnalyticEvent.listen(
      _handleAnalyticsEvent,
      onError: _handleError,
    );

    navigationSubscription = Userpilot.onNavigationEvent.listen(
      _handleNavigationEvent,
      onError: _handleError,
    );

    print('Subscribed to Userpilot streams');
  }

  // Detect if app was launched from a deeplink
  Future<String?> _startUri() async {
    // guard against processing initial link more than once
    if (!_initialURILinkHandled) {
      _initialURILinkHandled = true;
      return platform.invokeMethod('initialLink');
    }
    return null;
  }

  // Handle any deep link sent to the app
  Future<void> _onRedirected(String? url) async {
    if (!mounted || url == null) return;
    var uri = Uri.parse(url);
    // Pass along to Userpilot to potentially handle
    bool handled = await Userpilot.didHandleURL(uri);
    if (handled) return;
  }

  Future<void> _printUserpilotSettings() async {
    try {
      final settings = await Userpilot.settings();
      print('📋 Userpilot Settings:');
      settings.forEach((key, value) {
        print(' - $key: $value');
      });
    } catch (e) {
      print('Failed to fetch Userpilot settings: $e');
    }
  }

  void _handleExperienceEvent(UserpilotExperience event) {
    final parts = <String>[];
    parts.add('Type: ${event.experienceType}');

    if (event.experienceState != null) {
      parts.add('Experience State: ${event.experienceState}');
    }

    if (event.stepState != null) {
      parts.add('Step State: ${event.stepState}');
    }

    if (event.experienceId != null) {
      parts.add('Experience ID: ${event.experienceId}');
    }

    if (event.stepId != null) {
      parts.add('Step ID: ${event.stepId}');
    }

    if (event.step != null) {
      parts.add('Step: ${event.step}');
    }

    if (event.totalSteps != null) {
      parts.add('Total Steps: ${event.totalSteps}');
    }

    print('Experience Event: ${parts.join(', ')}');
  }

  void _handleAnalyticsEvent(UserpilotAnalytic event) {
    print('Analytics Event: ${event.analytic}, Value: ${event.value}');
  }

  void _handleNavigationEvent(UserpilotNavigation event) {
    print('Navigation Event URI: ${event.uri}');
    Uri uri = Uri.parse(event.uri);
    _processUri(uri);
  }

  // Process URI and handle navigation based on scheme and host
  void _processUri(Uri uri) {
    String scheme = uri.scheme;
    String host = uri.host;

    if (scheme == 'userpilot-USERPILOT_TOKEN') {
      if (host == "demo") {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          navigatorKey.currentState?.pushNamed(Routes.deepLink);
        });
      }
    } else {
      print('Unhandled navigation URI: ${uri.toString()}');
    }
  }

  void _handleError(Object error) {
    print('An error occurred: $error');
  }
}