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();
}
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;
}
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();
}
}
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();
}
}
}
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),
]);
});
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,
};
}
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...
}
The benefits of this approach are:
- Modularity: Easy to add or remove analytics providers without changing app logic
- Type Safety: Events are defined as classes, providing compile-time checks
- Separation of Concerns: Analytics logic is separate from business logic
- Consistency: Ensures consistent event tracking across the app
- 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!
Subscribe to my newsletter
Read articles from Dinko Marinac directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dinko Marinac
Dinko Marinac
Mobile app developer and consultant. CEO @ MOBILAPP Solutions. Passionate about the Dart & Flutter ecosystem.