Last Tuesday I bumped a fairly chunky MAUI app from .NET 9.0.4 to .NET 10.0.100, ran dotnet build -f net10.0-android, and watched the terminal vomit about 240 lines of Type androidx.lifecycle.ViewModelProvider$Factory is defined multiple times. Then androidx.core.app.ActivityCompat. Then androidx.fragment.app.Fragment. Same shape, different package, repeating forever. The iOS head built fine. Windows built fine. Only Android was on fire.
If you landed here from a stack trace that includes the phrase duplicate class androidx after a .NET 10 MAUI upgrade, this is almost certainly transitive AndroidX dependency drift — two of your NuGet packages are pulling in different versions of the same AndroidX Java library, and R8 is refusing to merge them. I have hit this exact failure three times in the last fortnight across two client codebases and one of my own apps, so I want to write down the diagnosis flow that actually works before I forget the painful bits.
The symptom: R8 fails with "Type ... is defined multiple times"
The error you will see in the build output looks something like this. I am quoting verbatim from a real upgrade I did on a Xamarin-era app that had been carried forward to MAUI:
/Users/amir/.nuget/packages/xamarin.androidx.core/1.13.1.5/buildTransitive/net8.0-android33.0/../../jar/androidx.core.core.jar
: Type androidx.core.app.ActivityCompat is defined multiple times:
/Users/amir/.nuget/packages/xamarin.androidx.activity/1.9.0.2/...
/Users/amir/.nuget/packages/xamarin.androidx.core/1.15.0.2/...
R8: Compilation can't be completed because the input contains
classes which are defined more than once.
Note the two different versions of Xamarin.AndroidX.Core in that output: 1.13.1.5 and 1.15.0.2. That is the whole story in one line. R8 (the Android shrinker that runs during MAUI's Android build, see the official R8 docs) is being handed two jars that both contain androidx.core.app.ActivityCompat, and it bails out rather than silently picking a winner. Old Xamarin.Android builds used to merge these with a "last write wins" strategy and you would discover the bug at runtime when a method signature was missing. R8 in .NET 10 is much stricter, which is genuinely better, but it also means upgrades that "worked" before now fail loudly.
The reason this manifests specifically on the .NET 9 to .NET 10 jump is that the MAUI workload's bundled Microsoft.Maui.Controls meta-package was rebuilt against newer Xamarin.AndroidX.* binding versions for .NET 10. If any of your other NuGet packages — Plugin.Firebase, CommunityToolkit.Maui.MediaElement, Plugin.Maui.Audio, third-party SDKs like Stripe or Mixpanel, even some QR scanner libraries — were built against the older AndroidX bindings, the resolver now picks both, because no single transitive constraint is strong enough to unify them.
Diagnosis: find which package is dragging the old AndroidX in
I waste a lot of time on this part if I try to read the error messages directly, because the duplicate-class output only shows the leaf jars, not the NuGet packages that brought them in. The fastest way I have found to get a real dependency tree is to ask the SDK for it:
cd src/MyApp
dotnet restore -f net10.0-android
dotnet list package --include-transitive \
--framework net10.0-android \
| grep -i androidx \
| sort -u
That gives you a flat list of every AndroidX package currently in the resolved graph, with versions. On the app I was debugging, the result included both Xamarin.AndroidX.Core 1.13.1.5 and Xamarin.AndroidX.Core 1.15.0.2. So now I needed to know who was asking for the older one. NuGet has a built-in command for that:
dotnet nuget why src/MyApp/MyApp.csproj Xamarin.AndroidX.Core \
--framework net10.0-android
This prints a real tree showing every path through your packages that reaches Xamarin.AndroidX.Core. In my case it surfaced the culprit immediately: ZXing.Net.Maui.Controls 0.4.0 depended on Xamarin.AndroidX.Activity (>= 1.9.0.2) which in turn pinned Xamarin.AndroidX.Core (>= 1.13.1.5). Meanwhile the .NET 10 MAUI workload was pulling Xamarin.AndroidX.Core 1.15.0.2 through Microsoft.Maui.Controls 10.0.0.
Two different floor versions, two different jars in the final APK, R8 unhappy. If dotnet nuget why is not available on your machine, check your SDK with dotnet --version — it shipped in the .NET 9 SDK and is still in 10 (Microsoft Learn reference).
The fix: pin AndroidX with central transitive pinning
The instinct is to upgrade the offending package — in my case, ZXing.Net.Maui.Controls — and hope a newer release targets the same AndroidX baseline as MAUI 10. Sometimes that works. Often it does not, because the maintainer has not cut a .NET 10 release yet, or there is no newer version at all.
The fix that has worked reliably for me is to explicitly pin the AndroidX libraries in the consuming project, which forces NuGet's resolver to use one version everywhere. There are two ways to do this. The blunt way is in your .csproj:
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.15.0.2" />
<PackageReference Include="Xamarin.AndroidX.Activity" Version="1.9.3.1" />
<PackageReference Include="Xamarin.AndroidX.Fragment" Version="1.8.5.1" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.8.7.1" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.8.7.1" />
</ItemGroup>
These versions are the ones currently shipped with the .NET 10 MAUI workload as of 2026. You can confirm the exact set your workload installed by running dotnet workload list and then poking around ~/.nuget/packages/microsoft.maui.controls.core/<version>/buildTransitive/net10.0-android/ — every AndroidX dependency MAUI itself ships against is listed there.
The better way, if you already use Directory.Packages.props for central package management, is to add the pins centrally and let every csproj inherit them. That is the setup I use across all the AutoContent sites and across MAUI apps with multiple platform projects:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Maui.Controls" Version="10.0.0" />
<PackageVersion Include="Xamarin.AndroidX.Core" Version="1.15.0.2" />
<PackageVersion Include="Xamarin.AndroidX.Activity" Version="1.9.3.1" />
<PackageVersion Include="Xamarin.AndroidX.Fragment" Version="1.8.5.1" />
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.8.7.1" />
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.8.7.1" />
</ItemGroup>
</Project>
The crucial line is CentralPackageTransitivePinningEnabled. Without it, central package management only pins direct references. With it, any PackageVersion entry also forces the version for transitive dependencies — exactly what we want here. The feature is described in the NuGet central package management docs.
After adding the pins, blow away the obj directories (the resolver caches are cached aggressively across SDK versions and will lie to you):
find . -type d \( -name bin -o -name obj \) -exec rm -rf {} +
dotnet restore
dotnet build -f net10.0-android -c Release
For my ZXing case the build went green on the first try after pinning. I have also seen this work for Plugin.Firebase 4.x and CommunityToolkit.Maui 9.x carrying old AndroidX dependencies into a .NET 10 project.
Caveats: when pinning is not enough
A couple of cases where the above does not fully solve the problem and you need to do more work.
The package depends on an AndroidX class that was actually removed. Sometimes Google deprecates and then deletes a class between AndroidX versions. If you pin AndroidX forward but your dependency was compiled against the older API, you will trade the duplicate-class error for a runtime NoSuchMethodError or VerifyError the first time that code path runs. The signal here is a build that succeeds but crashes on app start with a Java stack trace. The fix is to either find a newer version of the offending package (check the xamarin/AndroidX GitHub for the binding's status) or vendor the package and rebuild it locally against the new AndroidX baseline. I have only had to do the latter once and it was painful, but it worked.
R8 is not the only thing that can flag duplicates. The MAUI Android build also runs a step called <LinkAssemblies> and, before R8, a Java-level merger called <ResolveLibraryProjectImports>. If you see duplicates reported by those steps, the same diagnostic flow applies but the fix sometimes additionally requires deleting stale generated files under obj/Release/net10.0-android/lp/ — that is the "library projects" cache and it does not always invalidate cleanly when you change package versions.
You are using AOT and the duplicate is in a method body. Full AOT in MAUI 10 on Android is still preview as of mid-2026. If you have <PublishAot>true</PublishAot> or the new <RunAOTCompilation>true</RunAOTCompilation> flag set, the error messages will look slightly different and may reference the ILLink trimmer. The diagnosis (find the duplicate, pin one version) is the same; the cleanup commands are slightly different. I would not turn AOT on in the same PR as the .NET 10 upgrade — separate them so you can bisect.
Closing thoughts
Every time I do a major MAUI upgrade I rediscover that the framework's biggest sharp edge is not the rendering pipeline or hot reload — it is the Android dependency graph. The Java side of the build pulls in dozens of jars from Xamarin.AndroidX.* bindings, and any version mismatch fails late, loudly, and in a way that does not point at the actual package you need to change.
The recipe that has saved me hours is short: get the full transitive AndroidX list with dotnet list package --include-transitive, ask dotnet nuget why for the path to whichever package is duplicated, then pin the unified version centrally with CentralPackageTransitivePinningEnabled. If you are upgrading multiple MAUI apps this year, write yourself the same csproj snippet I have above and paste it in preemptively before you change the TargetFramework. You will thank yourself.
If you want to go deeper on the .NET 10 MAUI upgrade in general, I have also written about my full .NET 10 MAUI upgrade checklist and how I made MAUI Android builds twice as fast — both worth a read before you start the upgrade rather than after.
FAQ
Why did this not happen on .NET 9?
The Xamarin.AndroidX.* binding floor versions inside the MAUI workload jumped between .NET 9 and .NET 10. On .NET 9 the framework happened to align with the AndroidX versions that older third-party packages were built against. On .NET 10 the floor moved up, so two versions of each core AndroidX jar now coexist in your graph unless you pin them.
Can I just downgrade Xamarin.AndroidX.Core to match the older package?
Sometimes, but it usually fails differently. MAUI 10 itself uses APIs from the newer AndroidX Core, so pinning AndroidX down to the old version often breaks the framework's own code. Pin up, not down. If the third-party package then breaks at runtime, that is the signal to upgrade the package or replace it.
Does CentralPackageTransitivePinningEnabled have side effects?
Yes — it makes your PackageVersion entries authoritative for transitive deps, which means a dependency that legitimately needs a newer version than you have pinned will no longer be allowed to take it. In practice this is what you want for AndroidX, but check your build warnings after enabling it the first time.
Do I need to do this for iOS too?
No. The duplicate-class problem is specific to the Java/Android side of the build. iOS dependencies in MAUI flow through .NET assembly references and the runtime's normal binding redirects, which handle version conflicts differently and rarely produce hard build failures.