Mixpanel, Firebase, and Multi-Analytics setup in Flutter
How to send analytics events to multiple providers from a single source of truth

How to send analytics events to multiple providers from a single source of truth
If you often build something, you create some tools along the way. You design some blueprints that come in handy when you want to do something similar. This is exactly that.
For reading this article without a membership
Tracking user behavior is essential to understand how people actually use your app — not just how you think they do.
But when it comes to choosing an analytics tool, you’re often stuck with a trade-off: Firebase is great for out-of-the-box dashboards and Crashlytics, while Mixpanel gives you deep funnel analysis, retention breakdowns, and custom event properties. You might also have your custom-built event-tracking system in place.
So why not use all?
In this post, I’ll show you how to set up multi-provider analytics in Flutter. We’ll connect Firebase Analytics and Mixpanel, and build a shared interface so that you can log an event once and send it to as many services as you want behind the scenes.
Less boilerplate, more insight.
But why Multi-Analytics?
- You don’t want vendor lock-in.
- Some tools are better at different things.
- Product, marketing, and dev teams often need different types of data.
- You want flexibility without adding tech debt.
Rather than scattering logEvent calls everywhere, we’ll create an abstraction layer that cleanly sends data to multiple destinations.
Content
- Building a safe and abstract Analytics Provider
- Client-specific implementations
- A manager who manages all the implementations
- A singleton for accessibility over the project
Building a safe and abstract Analytics Provider

Let’s suppose you are building multiple houses. Think of your AnalyticsProvider as a blueprint — not for one house, but for how to build any house.
You’re the builder. And in your toolkit, you’ve got a standard design:
• Every house must have doors (logIn)
• Windows (logOut)
• A roof (logEvent)
That’s your abstract class — it defines what every analytics provider must include, no matter how it’s built or what tools it uses.
abstract class AnalyticsProvider {
Future<void> logEvent(String name, {Map<String, dynamic>? params});
Future<void> logIn(String userId, {Map<String, dynamic>? params});
Future<void> logOut();
}
This AnalyticsProvider class is an abstract interface — a contract that defines the structure any analytics implementation must follow in your app. The AnalyticsProvider class defines a common structure for analytics services (like Firebase, Mixpanel, or others). It ensures that every analytics backend you plug in behaves consistently and supports the same basic methods.
Instead of directly writing FirebaseAnalytics.logEvent(…) or Mixpanel.track(…) everywhere, you create provider classes that implement this interface, and then call the shared methods generically.
Before we move further, we must create an Event class and a Key class so that we don’t have the key and event names scattered throughout the app.
part 'analytics_keys.dart';
class AnalyticsConstants {
/// onboarding
static const String onboardingStartedEvent = 'User Onboarding Started';
...
}
part of 'analytics_constants.dart';
class AnalyticsKeys {
// user
static const String age = 'age';
...
}
Client-specific implementations

Now, say you’re building in different cities:
• Firebase is your Mumbai house — maybe it’s minimal, but integrated with the neighbourhood.
• Mixpanel is your Berlin house — more tracking rooms, and maybe some fancy behaviour sensors.
• Tomorrow, someone asks for Amplitude or Segment — you don’t panic. You’ve got the blueprint. You just build a new house using the same foundation.
The beauty?
All your clients (your app code) only need to know the blueprint — they don’t care which house they’re in.
They just say: “Hey, record this event” — and behind the scenes, all the right houses light up.
So let’s build your Firebase Analytics Provider. Just implement the Analytics Provider and voila!
import 'dart:convert';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:app/analytics/analytics_provider.dart';
import 'package:app/user_utils.dart';
class FirebaseAnalyticsProvider implements AnalyticsProvider {
final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
@override
Future<void> logEvent(String name, {Map<String, dynamic>? params}) {
try {
final parameters = <String, Object>{};
// add parameters
if (params != null && params.isNotEmpty) {
// filter out null values and convert remaining to Objects
// since firebase ananlytics does not allow null objects
params.forEach((key, value) {
if (value != null) {
parameters.addAll({
key: value
});
}
});
}
// Prepare the timestamp and add
String timeStamp = DateTime.now().toString();
parameters.addAll({
'timestamp': timeStamp,
});
// add user id
parameters.addAll({
'user_id': UserUtils.userId,
});
return _analytics.logEvent(
name: name, parameters: parameters);
} catch (ex) {
// failure to log event
return Future.value();
}
}
@override
Future<void> logIn(String userId, {Map<String, dynamic>? params}) {
_analytics.setUserId(id: userId);
if (params != null) {
params.forEach((key, value) {
_analytics.setUserProperty(name: key, value: value);
});
}
return Future.value();
}
@override
Future<void> logOut() {
// do nothing, firebase log out will handle it
return Future.value();
}
}
You can add a MixPanel Provider as well:
import 'dart:convert';
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
import 'package:app/analytics/analytics_provider.dart';
import 'package:app/user_utils.dart';
class MixpanelAnalyticsProvider implements AnalyticsProvider {
final Mixpanel analytics;
MixpanelAnalyticsProvider(this.analytics);
@override
Future<void> logEvent(String name, {Map<String, dynamic>? params}) {
try {
final parameters = <String, dynamic>{};
// add parameters
if (params != null && params.isNotEmpty) {
parameters.addAll(params);
}
// Prepare the timestamp and add
String timeStamp = DateTime.now().toString();
parameters.addAll({
'timestamp': timeStamp,
});
// add user id
parameters.addAll({
'user_id': UserUtils.userId,
});
return analytics.track(name, properties: parameters);
} catch (ex) {
// failure to log event
return Future.value();
}
}
@override
Future<void> logIn(String userId, {Map<String, dynamic>? params}) {
analytics.identify(userId);
if (params != null && params.isNotEmpty) {
params.forEach((key, value) {
analytics.getPeople().set(key, value);
});
}
return Future.value();
}
@override
Future<void> logOut() {
return analytics.reset();
}
}
And you can add any of your custom providers as well.
A manager who manages all the implementations

If the AnalyticsProvider is your blueprint, then the AnalyticsManager is the general contractor.
You, the app, don’t go talk to each builder (Mixpanel, Firebase) separately. You talk to the contractor, and he makes sure every house gets built the right way.
Each provider in _providers is a different house being built:
- Firebase house
- Mixpanel house
- Amplitude house (maybe one day)
When you call:
Analytics.instance.logEvent(AnalyticsKeys.productViewed, params: {...});
…it’s like saying:
“All houses need a skylight in the kitchen.”
And then the contractor (your AnalyticsManager) walks to each builder with the same instruction:
• Firebase adds a skylight
• Mixpanel adds a skylight
• Any other analytics house adds a skylight
You only made one request, but multiple houses got updated.
import 'package:app/analytics/analytics_provider.dart';
class AnalyticsManager {
final List<AnalyticsProvider> _providers;
AnalyticsManager(this._providers);
Future<void> logEvent(String name, {Map<String, dynamic>? params, Map<String, dynamic>? eventData}) async {
for (var provider in _providers) {
await provider.logEvent(name, params: params, eventData: eventData);
}
}
Future<void> login(String userId, {Map<String, dynamic>? params}) async {
for (var provider in _providers) {
await provider.logIn(userId, params: params);
}
}
Future<void> logOut() async {
for (var provider in _providers) {
await provider.logOut();
}
}
}
A singleton for accessibility over the project

This file is the construction site manager’s office. It’s where the crew gets assembled, blueprints are loaded, and the real building starts.
This is the Analytics HQ. It sets up:
- The contractor (AnalyticsManager)
- The houses (Firebase, Mixpanel, Custom tracking)
- The configuration (like choosing what features get built)
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
import 'package:app/analytics/analyics_manager.dart';
import 'package:app/analytics/custom_analytics_provider.dart';
import 'package:app/analytics/mixpanel_analytics_provider.dart';
import 'package:app/service/config/firebase_remote_config_service.dart';
class Analytics {
static late AnalyticsManager _instance;
static Future<void> init() async {
const mixpanelKey = kDebugMode
? 'debugKey'
: 'prodKey';
final mixpanel = await Mixpanel.init(
mixpanelKey,
trackAutomaticEvents: true,
);
_instance = AnalyticsManager([
CustomAnalyticsProvider(
acceptedEvents: _acceptedEvents,
),
MixpanelAnalyticsProvider(mixpanel)
]);
}
static AnalyticsManager get instance => _instance;
// This method fetches the accepted events from the remote config
// which are to be pushed to local analytics
static List<String> get _acceptedEvents {
final events = <String>[];
...
return events;
}
}
Now, you can initialise the Analytics in main.dart and use it like this:
Future<void> main() async {
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
// Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
await FirebaseRemoteConfigService.instance.initialize();
await Analytics.init();
runApp(MyApp(
themeCode: prefs.getString('theme_code'),
));
}, (error, stacktrace) {
debugPrint("Error $error $stacktrace");
});
}
Analytics.instance.logEvent(AnalyticsKeys.productViewed, params: {...});
Phew. You made it. You read the whole thing. That’s wild.
Thanks for sticking around — either you’re super curious or accidentally left your screen on while making coffee. Either way, I appreciate you.
Now, if this post helped, entertained, or mildly improved your day, feel free to smash that clap button. Medium lets you clap up to 50 times, which is… a weirdly high number. But hey, go wild. Pretend you’re giving a round of applause with both hands, twice.
No pressure, of course. But also… pressure.
Thanks again. You’re the best.

Thank you for being a part of the community
Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Newsletter | Podcast | Differ | Twitch
- Start your own free AI-powered blog on Differ 🚀
- Join our content creators community on Discord 🧑🏻💻
- For more content, visit plainenglish.io + stackademic.com