MocaLib2 Documentation
MocaLib2 is a Unity 6 modular SDK wrapper library by Titipi Games.
Each module wraps a third-party SDK behind a consistent, async-first interface.
Modules are individually enabled via scripting define symbols (MOCALIB2_USE_<MODULE>),
so only the modules you need are compiled into your build.
Installation
Add MocaLib2 to your project via the Unity Package Manager. Open Packages/manifest.json and add the entry under "dependencies":
{
"dependencies": {
"com.titipigames.unity-mocalib2": "git@titipi.github.com:TITIPI-GAMES/unity-mocalib-v2.git?path=Assets/Titipi/MocaLib2#v26.7.2",
...
}
}
Replace the tag (e.g. #v26.7.2) with any release tag or commit hash to pin a different version.
Enable modules
After importing, open Tools → MocaLib2 → Module Settings from the Unity menu bar. This opens a MocaLib2Config asset (created automatically if it doesn't exist) where you can toggle each module on or off. Scripting Define Symbols are updated automatically whenever you change a setting — no manual editing of Player Settings required.
To create a module config asset in Assets/_MocaLib2Configs/, use the corresponding Tools → MocaLib2 → Create <Module> Config menu item. The folder is created automatically if it doesn't exist. If the asset already exists it is selected and highlighted instead of recreated.
Firebase dependency.
FirebaseInAppMessagingModule,FirestoreLeaderboardModule, andFirestorePlayerProfileModulerequireFirebaseModuleto be enabled. Their checkboxes are grayed out in the settings UI until Firebase is turned on. Disabling Firebase automatically disables them.Network dependency.
IAPModulerequiresNetworkModule— it gates the purchase-verification timeout fallback on real device connectivity. Enabling IAP forcesMOCALIB2_USE_NETWORK_MODULEon, and the Network checkbox is shown checked-and-locked in the settings UI while IAP is enabled.
The table below lists the define symbol each module uses (for reference or CI overrides):
Modules
| Module | Define | Description |
|---|---|---|
| AdMobModule | MOCALIB2_USE_ADMOB_MODULE |
Google AdMob — Banner, Interstitial, Rewarded. Enable only one ad module per project. |
| AdjustModule | MOCALIB2_USE_ADJUST_MODULE |
Adjust — Attribution, events, ad revenue, purchase validation. Enable only one attribution module per project. |
| AppLovinModule | MOCALIB2_USE_APPLOVIN_MODULE |
AppLovin MAX — Banner, Interstitial, Rewarded. Enable only one ad module per project. |
| AppsFlyerModule | MOCALIB2_USE_APPSFLYER_MODULE |
AppsFlyer — Attribution, events, ad revenue, purchase validation. Enable only one attribution module per project. |
| ByteBrewModule | MOCALIB2_USE_BYTEBREW_MODULE |
ByteBrew — Custom events, user properties, purchases |
| ConsentModule | (always on) | iOS ATT consent; await before any SDK init. Forwards the outcome to Meta Audience Network when FacebookModule is enabled |
| ConfigModule | (always on) | Overlays Firebase Remote Config JSON onto module config assets at runtime |
| FacebookModule | MOCALIB2_USE_FACEBOOK_MODULE |
Facebook SDK init and app activation |
| FirebaseModule | MOCALIB2_USE_FIREBASE_MODULE |
Firebase — Analytics, Crashlytics, Remote Config, Push |
| FirebaseInAppMessagingModule | MOCALIB2_USE_FIREBASEINAPMESSAGING_MODULE |
Firebase — Native FIAM campaigns with local media caching |
| FirestoreLeaderboardModule | MOCALIB2_USE_FIREBASE_LEADERBOARD_MODULE |
Firestore leaderboards with server-side rank calculation |
| FirestorePlayerProfileModule | MOCALIB2_USE_FIREBASE_LEADERBOARD_MODULE |
Firestore-backed player profile with anonymous auth |
| GameAnalyticsModule | MOCALIB2_USE_GAMEANALYTICS_MODULE |
GameAnalytics — Design events, resource economy tracking |
| IAPModule | MOCALIB2_USE_IAP_MODULE |
Unity IAP v5 purchase flow (event-driven StoreController) with optional server validation |
| NetworkModule | MOCALIB2_USE_NETWORK_MODULE |
On-demand connectivity check and background polling. Required by IAPModule. |
| RatingModule | MOCALIB2_USE_RATING_MODULE |
Native in-app review / store rating prompt |
| ServerTimeModule | MOCALIB2_USE_SERVERTIME_MODULE |
Authoritative UTC time from Titipi server |
| UtilityModule | (always on) | Logging helpers, platform info, version, locale, device tier, IAP receipt parsing |
Special Feature Flags
These are fine-grained opt-ins that augment existing modules rather than adding a new one. They appear in a separate Special section in the Module Settings UI.
| Flag | Define | Description |
|---|---|---|
| CostCenter | MOCALIB2_USE_COST_CENTER |
Enables lifetime revenue tracking inside FirebaseModule — logs ad_revenue_sdk and iap_sdk events with cumulative totals stored in user properties. Requires FirebaseModule. |
Initialization Order
using Titipi.MocaLib2;
// 1. iOS ATT — must be first, before any SDK reads IDFA. Await it: the call returns only once the
// user has answered, so the SDKs below start with the final tracking state instead of racing the
// prompt. When FacebookModule is enabled it also hands the outcome to Meta Audience Network.
await ConsentModule.RequestATTrackingAsync();
// 2. Firebase — analytics, crashlytics, remote config, push; also a prerequisite
// for FirestoreLeaderboardModule, FirestorePlayerProfileModule, and FirebaseInAppMessagingModule
await FirebaseModule.InitializeAsync(firebaseConfig);
// 3. Attribution SDK — enable exactly one per project (AppsFlyer or Adjust, not both).
// It must follow Firebase so it can share the Firebase instance ID.
// Start it alongside Facebook, then await each, so their init times overlap.
var fbInit = FacebookModule.InitializeAsync();
var afInit = AppsFlyerModule.InitializeAsync(appsFlyerConfig);
// — or, if using Adjust instead:
// var adjInit = AdjustModule.InitializeAsync(adjustConfig);
await fbInit; await afInit;
// 4. Ad SDK — enable exactly one ad module per project (AppLovin or AdMob, not both).
// Pass the Firebase instance ID to AppLovin for cross-platform LTV attribution.
appLovinConfig.UserId = await FirebaseModule.GetAnalyticsInstanceIdAsync();
// To receive test ads during development, pass your test devices BEFORE init — a
// List<TestDevice>, typically parsed from the "ad_test_devices" Firebase Remote Config
// key (see the Sample App's LoadingController for the parsing). Remove before shipping.
// (AdMob offers AdMobModule.UseSampleAdUnits() for the same purpose during early dev.)
AppLovinModule.EnableTestAds(testDevices); // ← remove before release
// Fire-and-forget: mediation init does a real network round-trip and can take
// many seconds on a slow connection/device — don't block startup on it.
// UtilityModule.RunInBackground runs the work off the critical path and logs any
// exception instead of leaving it unobserved (the sanctioned place for async void).
UtilityModule.RunInBackground(async () =>
{
await AppLovinModule.InitializeAsync(appLovinConfig);
// — or, if using AdMob instead:
// await AdMobModule.InitializeAsync(adMobConfig);
// Requesting is manual — call only the formats you intend to show. Each returns
// immediately; the ad loads asynchronously and raises OnXxxAdLoaded when ready.
// Interstitial and Rewarded reload automatically after each show;
// cooldown and any other pacing logic lives in the game.
AppLovinModule.RequestBannerAd();
AppLovinModule.RequestInterstitialAd();
AppLovinModule.RequestRewardedAd();
});
// 5. Wire cross-module events (ad revenue → analytics, IAP → attribution). This includes
// IAPModule.OnRestorePurchases, which must be subscribed BEFORE step 6: purchases left
// unconfirmed by a previous session (crash mid-purchase, approved Ask-to-Buy) are
// redelivered during IAP init and stay unconfirmed until someone claims them.
WireEvents();
// 6. Unity IAP. Note: Ad SDK init (step 4) runs in the background and may not
// have finished yet, so any future ad-suppression logic tied to purchases
// must not assume the ad SDK is already initialized at this point.
await IAPModule.InitializeAsync(iapConfig, products);
// 7. Remaining SDKs — start them all first so they run concurrently, THEN await each.
// (Awaiting one-by-one on the same line would run them sequentially — the times would add up.)
var byteBrewInit = ByteBrewModule.InitializeAsync();
var gaInit = GameAnalyticsModule.InitializeAsync();
var ratingInit = RatingModule.InitializeAsync(ratingConfig);
var networkInit = NetworkModule.InitializeAsync();
var serverInit = ServerTimeModule.InitializeAsync();
await byteBrewInit; await gaInit; await ratingInit;
await networkInit;
NetworkModule.Enable(checkInterval: 15f, timeout: 5); // only after Network init has completed
await serverInit;
// 8. Firebase-dependent services — must follow step 2, but run concurrently with each other.
var leaderboardInit = FirestoreLeaderboardModule.InitializeAsync();
var playerProfileInit = FirestorePlayerProfileModule.InitializeAsync();
await leaderboardInit; await playerProfileInit;
Ad Events
AppLovinModule and AdMobModule expose the same event surface — only the type name differs, so swapping ad providers doesn't change your wiring:
| Event | Payload | Fires |
|---|---|---|
OnAdRevenuePaid |
AdImpressionData |
Every impression that earns revenue. Wire this to your analytics. |
OnBannerAdLoaded |
— | Banner finished loading |
OnBannerAdLoadFailed |
— | Banner failed to load |
OnBannerAdClicked |
— | User clicked the banner |
OnInterstitialAdLoaded |
— | Interstitial is ready to show (again after each auto-reload) |
OnInterstitialAdLoadFailed |
— | Interstitial failed to load — see transient note below |
OnInterstitialAdDisplayed |
— | Interstitial appeared on screen — mute audio / pause gameplay here |
OnInterstitialAdClicked |
— | User clicked the interstitial |
OnInterstitialAdClosed |
— | Interstitial was dismissed |
OnRewardedAdLoaded |
— | Rewarded is ready to show (again after each auto-reload) |
OnRewardedAdLoadFailed |
— | Rewarded failed to load — see transient note below |
OnRewardedAdDisplayed |
— | Rewarded appeared on screen — mute audio / pause gameplay here |
OnRewardedAdClicked |
— | User clicked the rewarded ad |
OnRewardedAdClosed |
— | Rewarded was dismissed — does not tell you whether the reward was earned |
Events are only raised after InitializeAsync completes (the module wires the SDK's callbacks once the SDK reports ready), so subscribing before init is safe — you cannot miss an event by subscribing early.
Load failures are transient, not terminal. OnInterstitialAdLoadFailed / OnRewardedAdLoadFailed fire on each failed attempt, and the module has already scheduled a capped exponential-backoff retry (2→64s). Treat them as "no ad right now," not "give up" — a later …AdLoaded will follow once one arrives.
There is deliberately no "display failed" or "reward earned" event. Those are one-shot outcomes of a specific show call, so they come back through the await instead — see below.
Ad revenue → analytics (step 5, WireEvents)
This is the wiring you should not skip. OnAdRevenuePaid is the seam between the ad module and the analytics modules: without it, no ad revenue reaches your analytics and LTV/ROAS reporting is silently empty. The ad module never calls analytics itself — that would couple modules that are toggled independently — so the caller connects them.
// Wire once at startup. Module events are static and live for the whole session.
private static void WireEvents()
{
// One event, many subscribers — it fans out to every analytics SDK you've enabled.
AppLovinModule.OnAdRevenuePaid += data => FirebaseModule.LogAdRevenue(data, "AppLovin");
AppLovinModule.OnAdRevenuePaid += AppsFlyerModule.LogAdRevenue; // signature matches; no lambda needed
// — or, if using Adjust instead:
// AppLovinModule.OnAdRevenuePaid += AdjustModule.LogAdRevenue;
// Purchases follow the same pattern
IAPModule.OnPurchaseSucceeded += (product, receipt) => AppsFlyerModule.LogPurchase(product, receipt);
// Crash-recovery / restore delivery: purchases left unconfirmed by a previous session are
// redelivered during IAP init. Grant idempotently — the store may redeliver an order twice.
IAPModule.OnRestorePurchases += (product, wasPendingHere) => GrantProduct(product);
}
AdMobModule is identical — AdMobModule.OnAdRevenuePaid += data => FirebaseModule.LogAdRevenue(data, "AdMob");.
A real project runs one ad module (AppLovin or AdMob) and one attribution SDK (AppsFlyer or Adjust), so there are four possible pairings. The sample app guards every combination with the matching MOCALIB2_USE_<MODULE> defines so it compiles whichever pair you pick — see LoadingController.WireEvents. Don't forget the attribution leg: wiring only Firebase is the easy mistake, and it leaves your attribution SDK with no ad revenue, which is what ROAS reporting is built on.
Wiring a module that is later disabled by the Enabled kill-switch is safe: every module method no-ops until initialized, so AppsFlyerModule.LogAdRevenue simply does nothing.
Pausing the game while a fullscreen ad is up
A fullscreen ad covers your game but does not pause it — audio keeps playing and gameplay keeps running underneath. The …Displayed / …Closed pair is the hook for that, and it's why you can't just do this around the await: the ad takes time to render and may fail to display, so "I called Show" is not the same moment as "the ad is on screen".
// Fires when the ad actually appears — not when you asked for it.
AppLovinModule.OnInterstitialAdDisplayed += PauseForAd;
AppLovinModule.OnInterstitialAdClosed += ResumeAfterAd;
AppLovinModule.OnRewardedAdDisplayed += PauseForAd;
AppLovinModule.OnRewardedAdClosed += ResumeAfterAd;
private void PauseForAd() { AudioListener.pause = true; Time.timeScale = 0f; }
private void ResumeAfterAd(){ AudioListener.pause = false; Time.timeScale = 1f; }
Reacting to ad availability
Use the …Loaded / …LoadFailed / …Closed events to keep UI in sync with what's actually ready. They fire repeatedly — interstitial and rewarded reload automatically after every show, and retry a failed load with backoff:
// Offer the reward button only while a rewarded ad is really available.
AppLovinModule.OnRewardedAdLoaded += () => _watchAdButton.interactable = true;
AppLovinModule.OnRewardedAdLoadFailed += () => _watchAdButton.interactable = false; // retry already scheduled
AppLovinModule.OnRewardedAdClosed += () => _watchAdButton.interactable = false; // reload starts immediately
Prefer a pull-style check instead? AppLovinModule.IsRewardedAdReady() answers the same question on demand.
Clicks
OnBannerAdClicked / OnInterstitialAdClicked / OnRewardedAdClicked are analytics-only — there's no other way to observe a click:
AppLovinModule.OnRewardedAdClicked += () => FirebaseModule.LogEvent("ad_click", "format", "rewarded");
Don't grant rewards from OnRewardedAdClosed
OnRewardedAdClosed fires on every dismissal — including when the user skipped the ad early — and carries no reward outcome. To learn whether the reward was actually earned, await the show call:
// ✅ "Did this user earn the reward?" — await the result.
if (await AppLovinModule.ShowRewardedAdAsync())
GiveCoins(50);
// ❌ OnRewardedAdClosed says the ad went away, not that it was watched.
AppLovinModule.OnRewardedAdClosed += () => GiveCoins(50); // pays out on a skip
This is the async-vs-event split in miniature: await for the outcome of one specific show; events for "tell me whenever."
Unsubscribe when the subscriber is a MonoBehaviour
The modules are static and outlive your scene objects, so a handler owned by a destroyed MonoBehaviour stays subscribed and fires into a dead object:
private void OnEnable() => AppLovinModule.OnRewardedAdLoaded += RefreshButton;
private void OnDestroy() => AppLovinModule.OnRewardedAdLoaded -= RefreshButton;
Startup wiring like WireEvents above needs no teardown — those handlers are meant to last the whole session.
Remote Config
When FirebaseModule is enabled with UseRemoteConfig, InitializeAsync fetches and activates Remote Config before it returns — bounded by RemoteConfigFetchTimeoutSeconds (default 10s) so a slow or dead network can't stall startup. Values are therefore ready to read the moment init completes; no callback is needed at startup — just read where you use them:
var welcome = FirebaseModule.GetRemoteString("welcome_message", "Hello!");
var maxLevel = FirebaseModule.GetRemoteInt("max_level", 100);
var flagOn = FirebaseModule.GetRemoteBool("new_feature_enabled", false);
To pull fresh values mid-session (e.g. when the app resumes from background), call RefreshRemoteConfigAsync() and re-read — or react via the OnRemoteConfigFetched event:
// Subscribe before InitializeAsync to also catch the initial fetch.
FirebaseModule.OnRemoteConfigFetched += success => { if (success) ApplyConfig(); };
if (await FirebaseModule.RefreshRemoteConfigAsync())
maxLevel = FirebaseModule.GetRemoteInt("max_level", 100);
Overriding Config from Remote Config
Every module config is a ScriptableObject you assign in the Inspector. ConfigModule.Resolve lets you overlay Remote Config JSON onto that asset at runtime, so you can retune any config field — ad unit ids, timeouts, feature flags — from the Firebase dashboard without shipping a new build. The asset supplies the defaults; Remote Config supplies partial or full overrides.
Route each config through Resolve when you initialize its module:
await AdMobModule.InitializeAsync(ConfigModule.Resolve(_adMobConfig));
Each config declares its own key via MocaLib2ModuleConfig.RemoteConfigKey (e.g. AdMobModuleConfig → "admob_config"). Set a Remote Config value for that key to a JSON object whose field names match the config's serialized fields:
{ "Android": { "BannerAdUnitId": "ca-app-pub-…/lowend-banner" } }
Resolve uses JsonUtility.FromJsonOverwrite, so only the fields present in the JSON are overridden — everything else keeps its asset value. It always returns a clone, so the shared asset is never mutated (safe even when you set runtime fields like AppLovinModuleConfig.UserId on the result).
Selecting a variant (device tier, region, A/B, …)
Resolve takes an optional keyOverride so a caller can pick a variant without the library needing to know what it means. Combined with a selector like UtilityModule.IsLowEndDevice, a caller can serve a different config to a subset of devices — for example a distinct set of ad unit ids to low-RAM devices:
var config = ConfigModule.Resolve(
_appLovinConfig,
UtilityModule.IsLowEndDevice ? "applovin_config_lowend" : null); // null → the config's own key
await AppLovinModule.InitializeAsync(config);
The variant key is entirely the caller's choice — device tier, region, A/B bucket, or anything else. UtilityModule.GetDeviceTier() (Low / Mid / High, from SystemInfo.systemMemorySize) is available if you need device buckets; the RAM cutoffs are parameters you can tune per game.
Runtime kill-switch (Enabled)
Leaf-module configs carry an Enabled flag (default true), defined on the ToggleableModuleConfig base. It's not exposed in the Inspector — it isn't a per-asset developer knob. A module that's compiled in (enabled via MocaLib2Config) simply runs; the flag exists solely so you can remotely turn a module off later via the Remote Config overlay, an emergency off-switch without shipping a build. Set { "Enabled": false } on the module's key to disable it fleet-wide — the caller then skips its initialization and the module's methods no-op. (Foundational configs like FirebaseModuleConfig extend MocaLib2ModuleConfig directly and deliberately have no Enabled field — they can't be gated off.)
The caller gates on it so dependent calls (ad requests, etc.) are skipped too:
var cfg = ConfigModule.Resolve(_appLovinConfig);
if (cfg.Enabled)
{
await AppLovinModule.InitializeAsync(cfg);
AppLovinModule.RequestBannerAd();
}
Enabled gates runtime initialization only — it does not remove the SDK's native libraries from the build (that's the compile-time MOCALIB2_USE_<MODULE> define). It's intended for leaf SDK modules (ads, attribution, rating, IAP). Foundational modules are deliberately not gated on it: FirebaseModule bootstraps Remote Config itself, and NetworkModule is a dependency of IAP. Disabling a leaf module whose events are still wired is safe — every module method no-ops (with a warning) until it's initialized, so handlers like AppsFlyerModule.LogAdRevenue simply do nothing.
Caveats
- Ordering. Resolution reads Remote Config, so it only works after
FirebaseModule.InitializeAsynchas fetched it — i.e. from step 3 onward.FirebaseModuleConfigitself therefore can't be remotely overridden (it bootstraps Remote Config); routing it throughResolveis a harmless no-op clone. - Partial overwrite is top-level only. A nested object present in the JSON (e.g.
"Android") replaces its counterpart wholesale, so specify all of that object's fields, not just the one you're changing. - Silent typos. A JSON field name that doesn't match a C# field is ignored — no error. Keep the JSON schema in sync with the config class. Malformed JSON is caught and logged, and the asset defaults are used.
- No Firebase, no override. When
FirebaseModuleis disabled or the key has no Remote Config value,Resolvereturns the asset's values unchanged.
Sample App
You can clone the full repo to have a working Unity Sample App that demonstrates how to use all the modules. The sample source code is placed in the Assets/_Game/Scripts folder.
A note about Async vs Callbacks
MocaLib2 is async-first: methods that complete once return Awaitable or Awaitable<T>. Methods that notify you of repeating events use callbacks or C# event. Knowing which pattern to reach for makes integration code simpler.
Async — "wait for one result"
Use async/await when an operation completes exactly once and the caller needs the result to proceed.
// Sequential steps read top-to-bottom — natural and easy to follow
bool rewarded = await AppLovinModule.ShowRewardedAdAsync();
if (rewarded) GiveCoins(50);
// Error handling works with plain try/catch. IapPurchaseException.Reason is a
// machine-readable code (e.g. "UserCancelled", "purchase_deferred") — no message parsing.
try
{
var product = await IAPModule.PurchaseProductAsync("com.example.coins");
UnlockContent(product);
}
catch (IapPurchaseException e) { ShowError(e.Reason); }
// Run independent operations concurrently, then collect results
var byteBrewInit = ByteBrewModule.InitializeAsync();
var gaInit = GameAnalyticsModule.InitializeAsync();
await byteBrewInit;
await gaInit;
Async shines when you chain several operations — no nested callbacks, no "callback hell":
// Hard to read as callbacks; trivial as async
await ShowIntroAnimationAsync();
await Awaitable.WaitForSecondsAsync(1f);
await SpawnEnemiesAsync();
StartGameplay();
Callbacks / Events — "notify me whenever"
Use callbacks or C# events for things that can happen zero, one, or many times — subscribing once and staying subscribed.
// Module-level events: wire once, receive every occurrence
IAPModule.OnPurchaseSucceeded += (product, receipt) =>
AppsFlyerModule.LogPurchase(product, receipt);
AppLovinModule.OnAdRevenuePaid += data =>
FirebaseModule.LogAdRevenue(data, "AppLovin");
// Optional outcome callbacks on longer-lived operations
FirebaseModule.RegisterForPushNotifications(
onGranted: () => ShowPushEnabledBadge(),
onDenied: () => ShowPushHint()
);
Quick reference
| Pattern | Use when… | MocaLib2 examples |
|---|---|---|
await |
Operation completes once; caller needs the result to continue | InitializeAsync, ShowRewardedAdAsync, PurchaseProductAsync, SaveProfileAsync |
Callback / event |
Event fires repeatedly or you need multiple subscribers | OnPurchaseSucceeded, OnAdRevenuePaid, RegisterForPushNotifications |
Rule of thumb: "I need a result" →
async. "Tell me every time this happens" → callback or event.
Conventions the modules follow
These are the rules every MocaLib2 module honors, so callers can rely on them:
Init returns success as a value, not an exception. Every InitializeAsync returns Awaitable<bool>: true on success, false on an expected failure (missing config, unresolved dependencies, unsupported platform). The failure reason is logged internally via UtilityModule.MocaLib2LogError, so you don't need a try/catch around init just to learn why it failed — check the bool if the result gates your next step, ignore it if it doesn't.
if (!await FirebaseModule.InitializeAsync(cfg))
Debug.LogError("Firebase failed — leaderboard/profile/FIAM unavailable.");
This is deliberately not a callback and not a result struct — bool + internal logging gives you explicit, value-typed success/failure while keeping linear await sequencing and single-site error handling. Reach for try/catch only for genuinely unexpected exceptions.
Events fire on the Unity main thread. All module events (OnAdRevenuePaid, OnInterstitialAdLoaded, OnPurchaseSucceeded, …) are raised on the main thread, so your handlers can touch Unity objects and other SDKs directly — no manual thread marshalling. (The ad modules do this internally where their SDKs raise events off-thread; e.g. AdMob marshals every callback through MobileAdsEventExecutor.ExecuteInUpdate.)
Fire-and-forget goes through UtilityModule.RunInBackground. When you want to start async work but not block the caller — e.g. warming up the ad SDK during startup so it doesn't hold up the loading screen — wrap it in RunInBackground. It runs the work off the critical path and logs any exception instead of leaving it unobserved. It is the one sanctioned place for async void in MocaLib2; don't write your own.
UtilityModule.RunInBackground(async () =>
{
await AdMobModule.InitializeAsync(adMobConfig);
AdMobModule.RequestBannerAd();
});
Parallelize init, but await before finishing the loading screen. Start independent SDKs concurrently (kick them all off, then await each) so their init times overlap instead of adding up — but do let them finish before you leave the loading screen, so everything is ready when the first screen appears. Respect real dependencies: Firebase before the ad user-id and Firestore modules, attribution SDKs after Firebase. The one exception is the ad SDK, which goes through RunInBackground: its init plus async ad fill can take several seconds and nothing on the first screen needs it, so blocking on it would only pad the loading screen for no benefit.