All insights
Xamarin → .NET MAUI

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.