Summary
proguard_xamarin.cfg — the config the SDK generates and passes to R8 on every $(AndroidLinkTool)=r8 build — contains an unconditional -dontobfuscate. There is no supported property to remove or override it, and no R8 CLI flag that can undo it. The result is that a .NET for Android app cannot produce an obfuscated classes*.dex at all.
That was a cosmetic difference until this month. Google Play has now announced a code-optimization requirement that makes it a compliance problem:
From February 2027, apps and games on Google Play will need to meet minimum optimization requirements. You will need to achieve a minimum of 25% optimization, obfuscation and shrinking for any app uploads to Play Console.
— https://support.google.com/googleplay/android-developer/answer/17492799
The requirement is scoped to non-negligible DEX: >10 MB for apps, >50 MB for games. Announcement: https://android-developers.googleblog.com/2026/08/app-quality-memory-optimization-secure-onboarding.html. Apps that miss the thresholds "may see store visibility impact" and may get a warning shown on their store listing.
Because obfuscation is one of the three metrics and .NET for Android pins it at zero, every .NET MAUI / .NET for Android app above 10 MB of DEX fails that metric by construction, regardless of how the developer configures their project.
Measured on a shipping app
.NET 10, Microsoft.Android.Sdk 36.1.69, $(AndroidLinkTool)=r8, Release, built on ubuntu-latest. Measured on the signed APK actually shipped to users:
classes.dex 8,570,180
classes2.dex 8,354,084
classes3.dex 7,644,760
classes4.dex 1,745,504
----------
26,314,528 bytes = 25.1 MB DEX
R8 markers are present in all four, and full mode is in use:
~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,
"min-api":26,"pg-map-id":"3bb8354","r8-mode":"full","version":"8.11.18"}
So shrinking and optimization do run. Obfuscation does not — zero renamed class descriptors in any of the four files:
Landroidx/activity/R$id;
Landroidx/activity/result/contract/ActivityResultContracts$PickVisualMedia$MediaCapabilities$$ExternalSyntheticApiModelOutline0;
...
Count of descriptors matching a minified shape (L(a-z0-9{1,2}/)+a-z0-9{1,2};): 0.
Root cause
proguard_xamarin.cfg is an embedded resource in Xamarin.Android.Build.Tasks.dll and begins:
# This is Xamarin-specific (and enhanced) configuration.
-dontobfuscate
-keep class android.support.multidex.MultiDexApplication { <init>(); }
-keep class net.dot.jni.** { *; <init>(); }
-keep class mono.MonoRuntimeProvider* { *; <init>(...); }
...
R8.cs writes it out inside CreateResponseFile() immediately before invoking r8, so no MSBuild target can get between generation and use. _CalculateProguardConfigurationFiles (Xamarin.Android.Common.targets) then includes it in @(_ProguardConfiguration), which Xamarin.Android.D8.targets passes as ProguardConfigurationFiles, and R8.cs emits one --pg-conf per entry.
The resource carries no comment explaining the flag, and I could not find an issue or PR discussing the decision.
Why the obvious workarounds do not work
1. $(AndroidR8ExtraArguments) cannot force minification back on. R8 8.11.18 (the version shipped in tools/r8.jar) has --no-tree-shaking, --no-minification, --no-data-resources and --no-desugaring, and no positive counterpart to any of them — there is no --minification / --obfuscate.
2. Appending another --pg-conf cannot cancel it. -dontobfuscate is order-independent and sticky, which I verified directly against the shipped r8.jar on a two-class fixture:
| configs passed |
result |
b.cfg (no -dontobfuscate) |
Ldemo/Helper; and secretMethodName both renamed |
a.cfg (-dontobfuscate) then b.cfg |
both names survive |
b.cfg then a.cfg (-dontobfuscate) last |
both names survive |
3. $(ProguardConfigFiles) "works" but is not a safe answer. It replaces the entire default list, so proguard_xamarin.cfg is never passed and -dontobfuscate never reaches R8. But the same list also carries proguard_project_primary.cfg (the generated -keep for every Android Callable Wrapper) and proguard_project_references.cfg. Re-listing those by hand is required for the app to launch at all, means maintaining a copy of an SDK-internal keep file that drifts silently against the SDK, and fails as a launch crash rather than a build error. The property is also undocumented — it appears on no page under https://learn.microsoft.com/dotnet/android/building-apps/build-properties (which has zero hits for "obfuscat" or "minif" anywhere), and its only description in the tree is the Common.targets comment "to keep backwards compatibility".
#11709 (merged 2026-06-23) already deals with disallowed global options in library proguard.txt files, and states that "the SDK-generated configs (proguard_xamarin.cfg, proguard_project_primary.cfg, etc.) are left untouched". So the neighbouring code was revisited recently and the global -dontobfuscate was deliberately retained — which is why I am raising this as its own issue rather than assuming it is an oversight.
Ask
Some supported way to opt in to R8 minification, with the SDK continuing to own the keeps that make it safe (ACWs, mono.*, net.dot.jni.*, bound types via GenerateProguardConfiguration). For example an $(AndroidEnableObfuscation) / $(AndroidR8Minification) property that omits -dontobfuscate from the generated config while leaving every generated -keep in place.
Two things that would help even without a full opt-in:
- Guidance. If obfuscation is unsafe for .NET for Android for a structural reason, documenting that reason — and Microsoft's position on the February 2027 requirement — would let teams plan. Right now the build properties page does not mention obfuscation at all.
- A supported seam, if the answer is "bring your own config": documenting
$(ProguardConfigFiles) and giving a way to append to @(_ProguardConfiguration) minus the one file, so nobody has to hand-copy generated keeps.
Happy to test a fix against a real 25 MB-DEX app and report the resulting Play Console numbers.
Environment
- .NET 10,
Microsoft.Android.Sdk.Windows 36.1.69 (tools/Version.commit: d549e1d, release/10.0.1xx)
- R8 8.11.18,
r8-mode: full, min-api: 26
- Release build on
ubuntu-latest; $(AndroidLinkTool)=r8, $(JavaMaximumHeapSize)=6G
Summary
proguard_xamarin.cfg— the config the SDK generates and passes to R8 on every$(AndroidLinkTool)=r8build — contains an unconditional-dontobfuscate. There is no supported property to remove or override it, and no R8 CLI flag that can undo it. The result is that a .NET for Android app cannot produce an obfuscatedclasses*.dexat all.That was a cosmetic difference until this month. Google Play has now announced a code-optimization requirement that makes it a compliance problem:
— https://support.google.com/googleplay/android-developer/answer/17492799
The requirement is scoped to non-negligible DEX: >10 MB for apps, >50 MB for games. Announcement: https://android-developers.googleblog.com/2026/08/app-quality-memory-optimization-secure-onboarding.html. Apps that miss the thresholds "may see store visibility impact" and may get a warning shown on their store listing.
Because obfuscation is one of the three metrics and .NET for Android pins it at zero, every .NET MAUI / .NET for Android app above 10 MB of DEX fails that metric by construction, regardless of how the developer configures their project.
Measured on a shipping app
.NET 10,
Microsoft.Android.Sdk36.1.69,$(AndroidLinkTool)=r8, Release, built onubuntu-latest. Measured on the signed APK actually shipped to users:R8 markers are present in all four, and full mode is in use:
So shrinking and optimization do run. Obfuscation does not — zero renamed class descriptors in any of the four files:
Count of descriptors matching a minified shape (
L(a-z0-9{1,2}/)+a-z0-9{1,2};): 0.Root cause
proguard_xamarin.cfgis an embedded resource inXamarin.Android.Build.Tasks.dlland begins:R8.cswrites it out insideCreateResponseFile()immediately before invokingr8, so no MSBuild target can get between generation and use._CalculateProguardConfigurationFiles(Xamarin.Android.Common.targets) then includes it in@(_ProguardConfiguration), whichXamarin.Android.D8.targetspasses asProguardConfigurationFiles, andR8.csemits one--pg-confper entry.The resource carries no comment explaining the flag, and I could not find an issue or PR discussing the decision.
Why the obvious workarounds do not work
1.
$(AndroidR8ExtraArguments)cannot force minification back on. R8 8.11.18 (the version shipped intools/r8.jar) has--no-tree-shaking,--no-minification,--no-data-resourcesand--no-desugaring, and no positive counterpart to any of them — there is no--minification/--obfuscate.2. Appending another
--pg-confcannot cancel it.-dontobfuscateis order-independent and sticky, which I verified directly against the shippedr8.jaron a two-class fixture:b.cfg(no-dontobfuscate)Ldemo/Helper;andsecretMethodNameboth renameda.cfg(-dontobfuscate) thenb.cfgb.cfgthena.cfg(-dontobfuscate) last3.
$(ProguardConfigFiles)"works" but is not a safe answer. It replaces the entire default list, soproguard_xamarin.cfgis never passed and-dontobfuscatenever reaches R8. But the same list also carriesproguard_project_primary.cfg(the generated-keepfor every Android Callable Wrapper) andproguard_project_references.cfg. Re-listing those by hand is required for the app to launch at all, means maintaining a copy of an SDK-internal keep file that drifts silently against the SDK, and fails as a launch crash rather than a build error. The property is also undocumented — it appears on no page under https://learn.microsoft.com/dotnet/android/building-apps/build-properties (which has zero hits for "obfuscat" or "minif" anywhere), and its only description in the tree is theCommon.targetscomment "to keep backwards compatibility".Note on #11709
#11709 (merged 2026-06-23) already deals with disallowed global options in library
proguard.txtfiles, and states that "the SDK-generated configs (proguard_xamarin.cfg,proguard_project_primary.cfg, etc.) are left untouched". So the neighbouring code was revisited recently and the global-dontobfuscatewas deliberately retained — which is why I am raising this as its own issue rather than assuming it is an oversight.Ask
Some supported way to opt in to R8 minification, with the SDK continuing to own the keeps that make it safe (ACWs,
mono.*,net.dot.jni.*, bound types viaGenerateProguardConfiguration). For example an$(AndroidEnableObfuscation)/$(AndroidR8Minification)property that omits-dontobfuscatefrom the generated config while leaving every generated-keepin place.Two things that would help even without a full opt-in:
$(ProguardConfigFiles)and giving a way to append to@(_ProguardConfiguration)minus the one file, so nobody has to hand-copy generated keeps.Happy to test a fix against a real 25 MB-DEX app and report the resulting Play Console numbers.
Environment
Microsoft.Android.Sdk.Windows36.1.69 (tools/Version.commit:d549e1d,release/10.0.1xx)r8-mode: full,min-api: 26ubuntu-latest;$(AndroidLinkTool)=r8,$(JavaMaximumHeapSize)=6G