-
Notifications
You must be signed in to change notification settings - Fork 27
test(sample): add opt-in deferred-init edge-case example (Android) #341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
nickolas-dimitrakas
merged 4 commits into
main
from
nickolas-dimitrakas/sample-deferred-init-example
Jun 19, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
99ece0b
test(sample): add opt-in deferred-init edge-case example (Android)
nickolas-dimitrakas f5e04be
fix(ci): apply ktlint formatting to sample deferred-init example
nickolas-dimitrakas 52e1059
fix(sample): gate deferred-init to Android and make startMParticle id…
nickolas-dimitrakas 5c643ac
docs(sample): note that pre-init JS calls no-op under deferred init
nickolas-dimitrakas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
sample/android/app/src/main/java/com/mparticlesample/ActivityTracker.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.mparticlesample | ||
|
|
||
| import android.app.Activity | ||
| import android.app.Application | ||
| import android.os.Bundle | ||
| import android.util.Log | ||
| import java.lang.ref.WeakReference | ||
|
|
||
| /** | ||
| * Mirrors the Rokt Android SDK's `ActivityLifeCycleObserver` caching strategy: | ||
| * the "current activity" is captured ONLY in onActivityResumed. This is the | ||
| * mechanism RoktModalActivity (overlay / bottom-sheet placements) depends on. | ||
| * | ||
| * We register two trackers in the repro: | ||
| * - EAGER : registered in Application.onCreate (before MainActivity exists) | ||
| * - DEFERRED: registered when MParticle.start() is called at first-frame paint | ||
| * | ||
| * If the deferred tracker is registered AFTER the host Activity has already | ||
| * resumed, it never sees onActivityResumed and currentActivity stays null -- | ||
| * exactly the failure ROKT/sdk-android-source#1062 / #1063 fix. | ||
| */ | ||
| class ActivityTracker(private val label: String) : Application.ActivityLifecycleCallbacks { | ||
|
|
||
| @Volatile | ||
| private var currentActivityRef: WeakReference<Activity>? = null | ||
|
|
||
| val currentActivity: Activity? | ||
| get() = currentActivityRef?.get() | ||
|
|
||
| companion object { | ||
| const val TAG = "DeferredInitRepro" | ||
| } | ||
|
|
||
| override fun onActivityResumed(activity: Activity) { | ||
| currentActivityRef = WeakReference(activity) | ||
| Log.i(TAG, "[$label] onActivityResumed -> captured ${activity.localClassName}") | ||
| } | ||
|
|
||
| override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { | ||
| Log.i(TAG, "[$label] onActivityCreated ${activity.localClassName}") | ||
| } | ||
|
|
||
| override fun onActivityStarted(activity: Activity) {} | ||
| override fun onActivityPaused(activity: Activity) {} | ||
| override fun onActivityStopped(activity: Activity) {} | ||
| override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} | ||
| override fun onActivityDestroyed(activity: Activity) {} | ||
| } |
115 changes: 115 additions & 0 deletions
115
sample/android/app/src/main/java/com/mparticlesample/DeferredInitModule.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| package com.mparticlesample | ||
|
|
||
| import android.app.Application | ||
| import android.os.Handler | ||
| import android.os.Looper | ||
| import android.util.Log | ||
| import com.facebook.react.bridge.Promise | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
| import com.facebook.react.bridge.ReactMethod | ||
| import com.mparticle.MParticle | ||
| import com.mparticle.MParticleOptions | ||
| import com.mparticle.identity.IdentityApiRequest | ||
|
|
||
| /** | ||
| * Edge-case example: "deferred initialisation with Turbo Modules". | ||
| * | ||
| * Instead of calling MParticle.start() in MainApplication.onCreate() (the standard, | ||
| * supported integration), JS calls startMParticle() from this native module after the | ||
| * first frame is painted. This reproduces a partner pattern used to shave init cost off | ||
| * app startup -- and exposes an Android-specific race that does NOT occur on iOS: | ||
| * | ||
| * The Rokt SDK caches its "current Activity" only in onActivityResumed, via an observer | ||
| * registered during Rokt.init() (which mParticle calls from RoktKit.onKitCreate()). When | ||
| * init runs AFTER the host Activity has already resumed -- exactly what deferred init does | ||
| * -- that resume is missed, so overlay/bottom-sheet placements (RoktModalActivity) have no | ||
| * Activity to launch from until the next resume (the "press home and reopen" workaround). | ||
| * iOS is immune because it resolves the presenter lazily at execute time. | ||
| * | ||
| * Fixed upstream in the Rokt Android SDK by observing the lifecycle from process start: | ||
| * ROKT/sdk-android-source#1062 and #1063 (v5 backport #1082). This example is kept around | ||
| * as an easy way to re-trigger the scenario and confirm the fix / catch regressions. | ||
| * | ||
| * To enable: flip DEFERRED_INIT_EXAMPLE to true and watch `adb logcat -s DeferredInitRepro` | ||
| * -- the DEFERRED tracker's currentActivity stays null while the EAGER one captures it. | ||
| * | ||
| * (Registered through a legacy ReactPackage; under the New Architecture it is invoked via the | ||
| * TurboModule interop layer. The JS call timing -- after first-frame paint -- is identical to | ||
| * a pure TurboModule, which is what the race depends on.) | ||
| */ | ||
| class DeferredInitModule(private val reactContext: ReactApplicationContext) : | ||
| ReactContextBaseJavaModule(reactContext) { | ||
|
|
||
| companion object { | ||
| const val TAG = "DeferredInitRepro" | ||
|
|
||
| /** | ||
| * Master switch for the deferred-init edge case. Keep this `false` so the sample uses the | ||
| * standard eager init in MainApplication.onCreate(); set it to `true` to reproduce the | ||
| * late-init Activity-capture race described above. | ||
| */ | ||
| const val DEFERRED_INIT_EXAMPLE = false | ||
|
|
||
| // Registered in Application.onCreate -- the eager path, for contrast. | ||
| var eagerTracker: ActivityTracker? = null | ||
| } | ||
|
|
||
| // Registered at deferred-start time, mirroring Rokt.init()'s late registration. | ||
| private val deferredTracker = ActivityTracker("DEFERRED") | ||
|
|
||
| override fun getName(): String = "DeferredInit" | ||
|
|
||
| @ReactMethod | ||
| fun startMParticle(promise: Promise) { | ||
| if (!DEFERRED_INIT_EXAMPLE) { | ||
| // Standard mode: mParticle was already started in Application.onCreate(); nothing to do. | ||
| promise.resolve("eager-init (deferred example disabled)") | ||
| return | ||
| } | ||
|
|
||
| // Idempotent: JS may invoke this more than once (Fast Refresh, remounts). Once mParticle | ||
| // has started, skip re-registering the lifecycle observer and re-starting the SDK. | ||
| if (MParticle.getInstance() != null) { | ||
| promise.resolve("already started") | ||
| return | ||
| } | ||
|
|
||
| Log.i(TAG, "startMParticle() called from JS (post first-frame). MParticle.getInstance()=${MParticle.getInstance()}") | ||
|
|
||
| val app = reactContext.applicationContext as Application | ||
|
|
||
| // Mirror Rokt SDK: register the activity observer at init time, NOT at process start. | ||
| app.registerActivityLifecycleCallbacks(deferredTracker) | ||
| Log.i(TAG, "[DEFERRED] tracker registered. currentActivity right now = ${deferredTracker.currentActivity}") | ||
|
|
||
| val identityRequest = IdentityApiRequest.withEmptyUser() | ||
| val options = MParticleOptions.builder(app) | ||
| .credentials("REPLACE_ME", "REPLACE_ME") | ||
| .logLevel(MParticle.LogLevel.VERBOSE) | ||
| .identify(identityRequest.build()) | ||
| .build() | ||
|
|
||
| MParticle.start(options) | ||
| Log.i(TAG, "MParticle.start() invoked from native module. getInstance()=${MParticle.getInstance()}") | ||
|
|
||
| // Snapshot the captured activity 2s later: did the deferred observer ever | ||
| // see a resume? (Rokt's currentActivity cache works exactly this way.) | ||
| Handler(Looper.getMainLooper()).postDelayed({ | ||
| Log.i( | ||
| TAG, | ||
| "SNAPSHOT after 2s -> EAGER.currentActivity=${eagerTracker?.currentActivity?.localClassName} | " + | ||
| "DEFERRED.currentActivity=${deferredTracker.currentActivity?.localClassName}" | ||
| ) | ||
| }, 2000) | ||
|
|
||
| promise.resolve("started") | ||
| } | ||
|
|
||
| @ReactMethod | ||
| fun reportActivityState(promise: Promise) { | ||
| val msg = "EAGER=${eagerTracker?.currentActivity?.localClassName} | DEFERRED=${deferredTracker.currentActivity?.localClassName}" | ||
| Log.i(TAG, "reportActivityState -> $msg") | ||
| promise.resolve(msg) | ||
| } | ||
| } |
14 changes: 14 additions & 0 deletions
14
sample/android/app/src/main/java/com/mparticlesample/DeferredInitPackage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.mparticlesample | ||
|
|
||
| import com.facebook.react.ReactPackage | ||
| import com.facebook.react.bridge.NativeModule | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.uimanager.ViewManager | ||
|
|
||
| class DeferredInitPackage : ReactPackage { | ||
| override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = | ||
| listOf(DeferredInitModule(reactContext)) | ||
|
|
||
| override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = | ||
| emptyList() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.