What replaces your Xamarin plugins in .NET MAUI
Almost nobody stalls on the UI conversion. Xamarin.Forms to .NET MAUI is mostly namespaces,
a new project file and a handler here and there, and the tooling does a lot of it. What stalls
the port is the dependency list — the fifteen or twenty Plugin.* packages the app quietly
depends on, most of which stopped getting commits somewhere around 2021.
The evaluation is where the months go. Somebody has to work out what replaces each one, whether the replacement covers the same platform behaviour, and whether it survives being backgrounded on a real device rather than in a simulator. That is not a research project you should be paying for, so here is the map.
The straightforward ones
These moved into the platform. If your app uses them, the replacement is already installed.
| Xamarin package | Replacement in .NET MAUI |
|---|---|
Xamarin.Essentials |
Built into MAUI — Microsoft.Maui.Essentials, no package reference needed |
Plugin.Connectivity |
Connectivity in Essentials |
Plugin.DeviceInfo |
DeviceInfo in Essentials |
Plugin.Permissions |
Permissions in Essentials |
Plugin.Media |
MediaPicker in Essentials |
Plugin.SecureStorage |
SecureStorage in Essentials |
Xamarin.Auth |
WebAuthenticator in Essentials |
Plugin.Geolocator |
Geolocation in Essentials — only for one-shot lookups, see below |
sqlite-net-pcl |
Unchanged, still maintained |
If your dependency list is entirely in this table, the migration really is mostly mechanical and you should get on with it.
The ones with no obvious owner
These are the packages that turn a two-month port into a six-month one, because the honest answer to “what replaces this?” used to be “nothing, write it yourself”.
| Xamarin package | What we maintain |
|---|---|
Plugin.BluetoothLE |
Shiny.BluetoothLE — central/client. Shiny.BluetoothLE.Hosting for peripheral/GATT server |
Plugin.FirebasePushNotification, Plugin.PushNotification |
Shiny.Push, Shiny.Push.FirebaseMessaging, Shiny.Push.AzureNotificationHubs |
Plugin.LocalNotifications |
Shiny.Notifications — channels, repeats, badges, interactive actions |
Plugin.Geolocator (background tracking, geofences) |
Shiny.Locations — GPS tracking, geofencing and motion activity |
Plugin.BackgroundJob, hand-rolled JobService/BGTaskScheduler code |
Shiny.Jobs |
Hand-rolled NSURLSession/WorkManager transfer code |
Shiny.Net.Http — background upload/download |
Plugin.Settings |
Shiny.Extensions.Stores |
ZXing.Net.Mobile |
Shiny.Maui.Controls.Barcodes |
Acr.UserDialogs |
Shiny.Maui.Controls — dialogs, toasts, overlays and the rest of the control suite |
Plugin.ContactService |
Shiny.Maui.ContactStore |
Plugin.SpeechRecognition, Plugin.TextToSpeech (beyond one-shot) |
Shiny.Speech — STT, TTS, wake word |
Full disclosure on two of those rows: Plugin.BluetoothLE and Acr.UserDialogs are ours as
well. Shiny is what those became. If you are on either, you are already running our code — the
migration path is one we have a direct obligation to keep working.
The three that are actually hard
Everything above is a lookup. These three are not, and they are where a migration estimate is usually wrong.
1. Background execution
This is the big one. In Xamarin, a lot of apps got away with a Task.Run and a prayer, because
older OS versions were more forgiving. They are not anymore. Android’s background restrictions,
Doze, and per-OEM battery savers — Samsung, Xiaomi and Huawei all ship their own on top of
Google’s — will kill work that is not registered with the OS scheduler. iOS gives you
BGTaskScheduler with budgets that are genuinely opaque and that shift between OS releases.
The symptom is nasty because it is invisible in QA: the app compiles, demos fine, passes testing on a charging device on Wi-Fi, and then quietly stops syncing for a subset of field users a week after release. If your Xamarin app did anything on a timer, budget real time here.
2. Bluetooth reconnection and permissions
BLE scanning and connecting is the easy part. What breaks is everything around it: Android 12
split Bluetooth permissions into BLUETOOTH_SCAN, BLUETOOTH_CONNECT and
BLUETOOTH_ADVERTISE, and neverForLocation changes what you have to declare and justify. On
iOS, state restoration — the app being killed and relaunched by the system when a peripheral
reappears — is a whole feature that most Xamarin codebases either never implemented or
implemented once and never tested.
If your device talks to hardware that stays paired for days, the reconnect and restore path is the part to prototype first, not last.
3. Offline sync
Anything that queued writes locally and flushed them later needs re-examining rather than
porting, because the failure mode is silent data loss and it shows up in production. The
questions to answer before you write any code: what happens when the app is killed mid-flush,
what is the conflict rule when the same record was edited on the server, and is the queue
durable across an OS-initiated termination. Shiny.Data.Sync
exists because we kept writing this by hand and kept getting the edge cases wrong.
How to sequence the port
The order that works, in our experience:
- Audit the dependency list first, before touching any UI. Everything above is a lookup; the value is in finding the handful of packages that are not.
- Prototype the risky subsystems on their own — background, BLE reconnect, sync — against real devices, off charger, on cellular. A week here reprices the whole project.
- Multi-target the shared libraries. This is usually the unlock that lets the old and new apps run side by side instead of forcing a big-bang cutover.
- Convert the UI last. It is the most visible work and the least risky, which is exactly why it should not go first — it burns the schedule while the real unknowns stay unknown.
- Ship platform by platform rather than freezing feature delivery for the whole migration.
The single most common estimating mistake we see is treating this as a UI project. The UI is the part you can see. The dependency list is the part that decides whether you finish.