Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2b5736f
WIP: event bridge wiring and mixpanel_flutter_common package
tylerjroach May 21, 2026
53ef6d1
chore(android): move EventBridge subscriber to Dispatchers.IO
tylerjroach May 21, 2026
4a0c020
feat(event-bridge): start native subscription lazily on first listener
tylerjroach May 21, 2026
dabae99
updates
tylerjroach May 29, 2026
05ed1b9
chore(mixpanel_flutter): revert dart format churn in mixpanel_flutter…
tylerjroach May 29, 2026
7306d59
test(jsonlogic): fail loudly on malformed fixture entries
tylerjroach May 29, 2026
4193db7
fix(android): bump Kotlin to 2.1.0 to consume mixpanel-android-common
tylerjroach May 29, 2026
af6d8c4
fix(android): use Kotlin 2.0.0 instead of 2.1.0
tylerjroach May 29, 2026
6d20500
fix(android): bump example app's Kotlin to 2.0.0
tylerjroach May 29, 2026
b869ae5
fix(android): skip Kotlin metadata version check for plugin compile
tylerjroach Jun 1, 2026
da196be
revert(android): undo unhelpful Kotlin version bumps
tylerjroach Jun 1, 2026
5c2ac31
Merge branch 'main' into feat/event-bridge-common-package
tylerjroach Jun 2, 2026
581a14d
optimization
tylerjroach Jun 3, 2026
286723c
lazy event bridge
tylerjroach Jun 3, 2026
dd59d1f
pr updates
tylerjroach Jun 4, 2026
5dedbbf
docs: cite mixpanel-swift-common as the precedent for === int precision
tylerjroach Jun 4, 2026
0e4f50c
test: cover MissingPluginException, lifecycle error swallowing, and i…
tylerjroach Jun 4, 2026
36033fe
event bridge testing
tylerjroach Jun 8, 2026
06efb5b
Merge branch 'main' into feat/event-bridge-common-package
tylerjroach Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/mixpanel_flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ group 'com.mixpanel.mixpanel_flutter'
version '1.0'

buildscript {
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
Expand All @@ -10,6 +11,7 @@ buildscript {
dependencies {
// Use a compatible version of Gradle Plugin
classpath 'com.android.tools.build:gradle:8.1.0' // Updated to 8.1.0
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

Expand All @@ -21,6 +23,7 @@ rootProject.allprojects {
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
// Safely handle 'namespace' property for new Android Gradle plugin versions
Expand All @@ -40,12 +43,26 @@ android {
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = '17'
}

sourceSets {
main.kotlin.srcDirs += 'src/main/kotlin'
}

lintOptions {
disable 'InvalidPackage'
}
}

dependencies {
// Use the Mixpanel Android SDK
implementation "com.mixpanel.android:mixpanel-android:8.7.0"
// mixpanel-android:8.7.0 only declares mixpanel-android-common as a
// runtime dependency, so MixpanelEventBridge is not on the compile
// classpath unless we add it explicitly here. EventBridgeSubscriber.kt
// imports it directly.
implementation "com.mixpanel.android:mixpanel-android-common:1.0.1"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,30 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
case "getAllVariants":
handleGetAllVariants(call, result);
break;
case "startEventBridge":
handleStartEventBridge(result);
break;
case "stopEventBridge":
handleStopEventBridge(result);
break;
default:
result.notImplemented();
break;
}
}

private void handleStartEventBridge(Result result) {
if (channel != null) {
EventBridgeSubscriber.start(channel);
}
result.success(null);
}

private void handleStopEventBridge(Result result) {
EventBridgeSubscriber.stop();
result.success(null);
}

private void initializeMethodChannel() {
if (channel == null && flutterPluginBinding != null) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "mixpanel_flutter",
Expand Down Expand Up @@ -785,6 +803,7 @@ private long readPersistenceTtlMillis(Map<String, Object> policyMap) {

@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
EventBridgeSubscriber.stop();
if (channel != null) {
channel.setMethodCallHandler(null);
channel = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.mixpanel.mixpanel_flutter

import android.util.Log
import com.mixpanel.android.eventbridge.MixpanelEventBridge
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.json.JSONException
import org.json.JSONObject

/**
* Subscribes to the native Mixpanel SDK's [MixpanelEventBridge] (a Kotlin
* `SharedFlow`) and forwards each event to the Dart side via the existing
* Flutter MethodChannel.
*
* The Java plugin calls [start] from `onAttachedToEngine` and [stop] from
* `onDetachedFromEngine`. This object is a singleton because the native
* SharedFlow itself is a singleton — we never want more than one active
* subscription per process.
*/
object EventBridgeSubscriber {

private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var job: Job? = null

@JvmStatic
fun start(channel: MethodChannel) {
if (job != null) return
job = scope.launch {
MixpanelEventBridge.events().collect { event ->
val properties = event.properties?.let { safelyConvert(it) }
channel.invokeMethod(
"onMixpanelEvent",
mapOf(
"eventName" to event.eventName,
"properties" to properties,
)
)
}
}
}

@JvmStatic
fun stop() {
job?.cancel()
job = null
}

private fun safelyConvert(json: JSONObject): Map<String, Any?>? = try {
MixpanelFlutterHelper.toMap(json)
} catch (e: JSONException) {
// A malformed properties payload should not abort the whole
// subscription — drop this event's properties and keep collecting.
Log.w("EventBridgeSubscriber", "Failed to convert event properties", e)
null
}
}
7 changes: 7 additions & 0 deletions packages/mixpanel_flutter/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ packages:
relative: true
source: path
version: "2.8.0"
mixpanel_flutter_common:
dependency: transitive
description:
path: "../../mixpanel_flutter_common"
relative: true
source: path
version: "0.1.0"
path:
dependency: transitive
description:
Expand Down
3 changes: 3 additions & 0 deletions packages/mixpanel_flutter/ios/mixpanel_flutter.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Pod::Spec.new do |s|
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'Mixpanel-swift', '6.4.0'
# Explicit dependency (also pulled in transitively by Mixpanel-swift 6.4+)
# so `import MixpanelSwiftCommon` in our plugin resolves reliably.
s.dependency 'MixpanelSwiftCommon', '~> 1.0.0'
s.platform = :ios, '12.0'

# Flutter.framework does not contain a i386 slice.
Expand Down
Loading
Loading