Integrating Multiple Analytics Services in Flutter: The Modular Way

dinko7

Dinko Marinac

Posted on July 29, 2024

Integrating Multiple Analytics Services in Flutter: The Modular Way

Introduction

In the world of mobile app development, analytics play a crucial role in understanding user behavior and making data-driven decisions.

However, integrating analytics into your Flutter app can often lead to tightly coupled code and difficulty in switching between analytics providers.

Migrating analytics is not like turning a switch and it takes time for data to settle in the new platforms, while at the same time, the data is still needed to make decisions.

To ease the migration and management of 2 (and possibly more platforms) I’ve used the following modular approach which I will show here with Mixpanel and PostHog as examples.

Ready to integrate some analytics?

Full code can be found on GitHub.

Interface and event

To use multiple analytics services, we need to create a common interface so we can talk to each of the platforms in a unified way.

abstract class AnalyticsService {
  void track(AnalyticsEvent event);
  void identify(String userId);
  void reset();
}
Enter fullscreen mode Exit fullscreen mode

This interface defines the core functionality we expect from any analytics service: tracking, identifying with the user upon login, and resetting upon logout.

Next, we need to define a generic event class:

abstract class AnalyticsEvent {
  String get eventName;
  Map<String, dynamic> get properties;
}
Enter fullscreen mode Exit fullscreen mode

Let’s integrate this with our services.

Concrete implementation

Before you start the implementation, make sure you follow the official guides on how to integrate Mixpanel and Posthog.

Concrete implementations of these services are just proxy calls to the underlying platform.

class MixpanelAnalyticsService implements AnalyticsService {
  final Mixpanel _mixpanel;

  MixpanelAnalyticsService(this._mixpanel);

  @override
  void track(AnalyticsEvent event) {
    _mixpanel.track(event.eventName, properties: event.properties);
  }

  @override
  void identify(String userId) {
    _mixpanel.identify(userId);
  }

  @override
  void reset() {
    _mixpanel.reset();
  }
}

class PostHogAnalyticsService implements AnalyticsService {
  final Posthog _posthog;

  PostHogAnalyticsService(this._posthog);

  @override
  void track(AnalyticsEvent event) {
    _posthog.capture(
      eventName: event.eventName,
      properties: event.properties.map((key, value) => MapEntry(key, value as Object)),
    );
  }

  @override
  void identify(String userId) {
    _posthog.identify(userId: userId);
  }

  @override
  void reset() {
    _posthog.reset();
  }
}
Enter fullscreen mode Exit fullscreen mode

To be able to swap out analytics on the fly or use multiple services at once, we need a service that will encapsulate all the services. Let’s call it CompoundAnalyticsService.

class CompoundAnalyticsService implements AnalyticsService {
  final List<AnalyticsService> _services;

  CompoundAnalyticsService(this._services);

  @override
  void track(AnalyticsEvent event) {
    for (var service in _services) {
      service.track(event);
    }
  }

  @override
  void identify(String userId) {
    for (var service in _services) {
      service.identify(userId);
    }
  }

  @override
  void reset() {
    for (var service in _services) {
      service.reset();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Additionally, if you are using a state management solution and feature flags, you can turn on and off particular analytics on the fly. Here’s an example with Riverpod:

final _mixpanelAnalyticsServiceProvider = Provider<AnalyticsService>((ref) {
  final mixpanel = ref.watch(mixpanelProvider);
  return MixpanelAnalyticsService(mixpanel);
});

final _posthogAnalyticsServiceProvider = Provider<AnalyticsService>((ref) {
  final posthog = ref.watch(posthogProvider);
  return PostHogAnalyticsService(posthog);
});

final analyticsServiceProvider = Provider<AnalyticsService>((ref) {
  final flags = ref.watch(featureFlagsProvider);
  return CompoundAnalyticsService([
    if (flags.enableMixpanel) ref.watch(_mixpanelAnalyticsServiceProvider),
    if (flags.enablePosthog) ref.watch(_posthogAnalyticsServiceProvider),
  ]);
});
Enter fullscreen mode Exit fullscreen mode

Usage

To start using all of this we need an event. Let’s say we want to share something in the app: a photo, video, or another app.

enum ShareEventItem { photo, video, app }

class ShareEvent implements AnalyticsEvent {
  final ShareEventItem item;

  ShareEvent({required this.item});

  @override
  String get eventName => 'Share';

  @override
  Map<String, dynamic> get properties => {
        'item': item.toString().split('.').last,
      };
}
Enter fullscreen mode Exit fullscreen mode

Now, we can use our analytics service to track that even to any number of platforms like this:

class MyHomePage extends StatelessWidget {
  final AnalyticsService analyticsService;

  const MyHomePage({Key? key, required this.analyticsService}) : super(key: key);

  void _onShareItem(ShareEventItem item) {
    analyticsService.track(ShareEvent(item: item));
// Perform actual share action
  }

// Widget build method...
}

Enter fullscreen mode Exit fullscreen mode

The benefits of this approach are:

  1. Modularity: Easy to add or remove analytics providers without changing app logic
  2. Type Safety: Events are defined as classes, providing compile-time checks
  3. Separation of Concerns: Analytics logic is separate from business logic
  4. Consistency: Ensures consistent event tracking across the app
  5. Testability: Easy to mock analytics service for unit testing

Conclusion

By implementing this modular approach to analytics in your Flutter app, you can enjoy greater flexibility, maintainability, and consistency in your analytics implementation. It allows you to easily switch between or combine different analytics providers, ensuring that your app's analytics strategy can evolve with your needs.

Remember, the key to successful analytics is not just in the implementation, but in defining meaningful events that provide actionable insights for your app's growth and improvement.

If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on Twitter and LinkedIn.

Until next time, happy coding!

💖 💪 🙅 🚩
dinko7
Dinko Marinac

Posted on July 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related