Migracja Xamarin.Forms do .NET MAUI 10: przewodnik 2026
Praktyczny przewodnik migracji z Xamarin.Forms do .NET MAUI 10 w 2026: Upgrade Assistant, przepisanie Handlers, DI zamiast DependencyService, Shell, publikacja w App Store i Google Play.
Migracja z Xamarin.Forms do .NET MAUI 10 to obowiązkowy krok dla każdego zespołu utrzymującego mobilną aplikację w 2026 roku. Xamarin.Forms wyszedł ze wsparcia 1 maja 2024, a Xcode 16 oraz Android 15 nie kompilują już projektów opartych na tym stosie. W praktyce migracja polega na trzech rzeczach: uruchomieniu .NET Upgrade Assistant, ręcznym przepisaniu Custom Renderers na Handlers oraz podmianie DependencyService i MessagingCenter na wbudowany kontener DI i WeakReferenceMessenger. Ten tekst prowadzi Cię przez cały proces, tak jak robimy to w moim zespole (i szczerze mówiąc, tak jak żałowałem, że nie robiliśmy tego rok wcześniej).
Xamarin.Forms nie otrzymuje już poprawek bezpieczeństwa od 1 maja 2024, a nowy Xcode ani Google Play nie przyjmą jego builda bez obejść.
Pozostałe ~30% to ręczne przepisanie Custom Renderers na Handlers, DependencyService na Microsoft.Extensions.DependencyInjection oraz MessagingCenter na WeakReferenceMessenger z CommunityToolkit.Mvvm.
Dla średniej aplikacji (50–150 ekranów) planujemy 4–8 tygodni pracy jednego seniora. Nie próbuj mieszać obu stosów w jednym rozwiązaniu.
Efektem końcowym jest jeden projekt SDK-style z target frameworks net10.0-android i net10.0-ios, gotowy do publikacji w Google Play i App Store.
Dlaczego musisz zmigrować z Xamarin.Forms w 2026
Zacznijmy od twardych faktów, bo w rozmowach z klientami wciąż spotykam osoby, które myślą, że mają jeszcze czas. Nie mają. Wsparcie dla Xamarin.Forms zakończyło się 1 maja 2024 roku. Od tego momentu nie ma poprawek bezpieczeństwa, nowych wersji, wsparcia dla nowych API Androida ani iOS-a. To już nie jest kwestia „modernizacji”; to kwestia utrzymania aplikacji, która w ogóle się kompiluje.
Konkretne bolączki, na które trafiliśmy w moim zespole w ostatnich miesiącach? Xcode 16 (wymagany do publikacji od kwietnia 2025) nie obsługuje starych bindingów Xamarin.iOS. Google Play od sierpnia 2024 wymaga target SDK 34, a Xamarin.Android ma z tym problemy. SafetyNet został wyłączony i zastąpiony Play Integrity API, którego Xamarin nie obsługuje natywnie. Dodatkowo pakiety NuGet oparte na Xamarin.Forms przestały być aktualizowane: MvvmCross, Prism, AppCenter (który zresztą został wygaszony w marcu 2025).
Z perspektywy zespołu problem jest jeszcze głębszy. Nowi programiści .NET nie znają Xamarina, bo dokumentacja i tutoriale nie są aktualizowane. Onboarding trwa dłużej, a każdy bug wymaga grzebania w archiwalnych issue na GitHubie. Migracja do .NET MAUI 10 to nie jest „przyszłościowa inwestycja”, tylko spłacenie długu technicznego, który już generuje odsetki. Więcej o wydajności nowego stosu opisaliśmy w naszym przewodniku po optymalizacji wydajności .NET MAUI.
Xamarin.Forms vs .NET MAUI: kluczowe różnice
Zanim usiądziesz do migracji, warto zrozumieć, co dokładnie się zmienia. .NET MAUI nie jest kosmetycznym rebrandingiem Xamarina; to poważna rearchitektura silnika UI opartego na tzw. Handlerach zamiast Renderers. Poniższa tabela pokazuje najważniejsze różnice, które codziennie odczujesz w kodzie.
Wymiar
Xamarin.Forms
.NET MAUI 10
Struktura projektu
3–5 projektów (shared, Android, iOS, UWP)
Jeden projekt SDK-style z multi-targeting
Target framework
MonoAndroid / Xamarin.iOS
net10.0-android, net10.0-ios, net10.0-maccatalyst
Silnik UI
Custom Renderers
Handlers + Mappers
Wstrzykiwanie zależności
DependencyService (statyczny lookup)
Microsoft.Extensions.DependencyInjection
Komunikacja między klasami
MessagingCenter
WeakReferenceMessenger (CommunityToolkit.Mvvm)
Startup
App.xaml.cs + FormsAppCompatActivity
MauiProgram.CreateMauiApp() + host builder
Wsparcie oficjalne
Zakończone 01.05.2024
Aktywnie rozwijane, LTS w .NET 10
AOT / trimming
Ograniczone
Pełne NativeAOT na iOS (podgląd na Androidzie)
Najtrudniejsza koncepcyjnie zmiana to Handlers. W Xamarin.Forms Custom Renderer dziedziczył po platformowej klasie renderera i miał bezpośredni dostęp do natywnej kontrolki. W MAUI Handler to lekka klasa mostkująca, która używa statycznego PropertyMapper do wiązania właściwości cross-platform z natywnymi. Ta zmiana daje ogromny zysk wydajnościowy (mniej alokacji przy tworzeniu widoków), ale wymaga innego myślenia przy porcie własnych kontrolek.
Przygotowanie projektu przed migracją
Ostatnie, czego chcesz, to zacząć migrację i utknąć trzeciego dnia, bo w projekcie masz siedem starych zależności bez wersji MAUI. Zawsze zaczynamy od inwentaryzacji. W praktyce oznacza to trzy równoległe listy: pakiety NuGet, Custom Renderers/Effects oraz platformowy kod natywny.
# 1. Wygeneruj listę wszystkich NuGetów w projekcie
dotnet list package --format json > packages-audit.json
# 2. Zainstaluj .NET Upgrade Assistant globalnie
dotnet tool install -g upgrade-assistant
# 3. Sprawdź .NET 10 SDK (wymagany do MAUI 10)
dotnet --list-sdks
# Powinieneś zobaczyć: 10.0.100 lub nowszy
# 4. Workload MAUI
dotnet workload install maui
dotnet workload list
Dla każdego pakietu z listy sprawdź na NuGet.org, czy istnieje wersja kompatybilna z net10.0. Pakiety, na które trafiamy najczęściej i które mają odpowiedniki: Xamarin.Essentials (wbudowane w MAUI jako Microsoft.Maui.Essentials), Prism.Forms (Prism.Maui), Rg.Plugins.Popup (Mopups lub The49.Maui.BottomSheet), Refit (bez zmian), Polly (bez zmian). Problematyczne? MvvmCross (przejdź na CommunityToolkit.Mvvm), Akavache (rozważ zamianę na SQLite-net-pcl plus własną warstwę cache), Xamarin.Auth (zastąp WebAuthenticator z Essentials).
Drugi krok przygotowania to testy. Bez zestawu testów regresyjnych migracja staje się grą w rosyjską ruletkę (mówię z doświadczenia). Jeśli Twoja aplikacja ma mniej niż 30% pokrycia, poświęć tydzień na napisanie przynajmniej smoke testów dla golden path: ekran logowania, główny flow biznesowy, ekran płatności. O tym, jak podchodzimy do testów w nowym stosie, piszemy w naszym przewodniku po testowaniu .NET MAUI z xUnit i Appium.
.NET Upgrade Assistant krok po kroku
.NET Upgrade Assistant to oficjalne narzędzie Microsoftu, które automatyzuje mechaniczne części migracji. Nie zrobi wszystkiego, ale załatwia najbardziej żmudne rzeczy: konwersję csproj do stylu SDK, zmianę namespace’ów Xamarin.Forms.* na Microsoft.Maui.Controls.*, migrację XAML i podstawową restrukturyzację folderów. Oficjalną dokumentację znajdziesz w przewodniku migracji na Microsoft Learn.
# 1. Uruchom w katalogu głównym rozwiązania (obok pliku .sln)
upgrade-assistant upgrade MyApp.sln
# 2. Wybierz projekt startowy. Upgrade Assistant zapyta o entry point.
# Zwykle to projekt Android lub iOS.
# 3. Dla każdego projektu wybierz "Upgrade project to .NET 10".
# Narzędzie zaproponuje serię kroków, akceptuj kolejno:
# - Back up project
# - Convert project file to SDK style
# - Clean up NuGet references
# - Update TFM (Target Framework Moniker)
# - Update NuGet Packages
# - Add template files
# - Migrate XAML namespaces
Po uruchomieniu narzędzia dostajesz projekt, który się kompiluje, ale prawdopodobnie nie działa. To normalne. Upgrade Assistant zajmuje się strukturą, nie semantyką. W naszym ostatnim projekcie (~120 ekranów, 8 lat kodu) narzędzie przeszło przez pierwszy etap w 40 minut, zostawiając nam listę 340 błędów kompilacji. Większość z nich to zmiany namespace’ów i zniknięcia klas, więc w kilka godzin zredukowaliśmy to do 45 błędów wymagających ręcznej interwencji.
Warto też uruchomić try-convert na projektach platformowych (Android/iOS), które nie są głównym targetem. Czasem narzędzie potrafi je przekonwertować niezależnie, co daje szybsze pierwsze uruchomienie na jednej platformie.
Migracja Custom Renderers do Handlers
To jest część, którą Upgrade Assistant zostawi Tobie. W Xamarin.Forms typowy Custom Renderer dla przycisku wyglądał tak:
// Xamarin.Forms - Android renderer
[assembly: ExportRenderer(typeof(RoundedButton), typeof(RoundedButtonRenderer))]
public class RoundedButtonRenderer : ButtonRenderer
{
public RoundedButtonRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (Control != null)
{
var gd = new GradientDrawable();
gd.SetCornerRadius(30);
gd.SetColor(Color.ParseColor("#2196F3"));
Control.SetBackground(gd);
}
}
}
W .NET MAUI ten sam efekt osiągamy przez rozszerzenie ButtonHandler i modyfikację jego PropertyMapper. Zwróć uwagę, że kod platformowy siedzi teraz w folderze Platforms/Android/, a nie w osobnym projekcie:
// .NET MAUI - Handler dla wszystkich platform
public partial class RoundedButtonHandler : ButtonHandler
{
public static readonly IPropertyMapper<RoundedButton, RoundedButtonHandler> RoundedMapper =
new PropertyMapper<RoundedButton, RoundedButtonHandler>(Mapper)
{
[nameof(RoundedButton.CornerRadius)] = MapCornerRadius
};
public RoundedButtonHandler() : base(RoundedMapper) { }
static void MapCornerRadius(RoundedButtonHandler handler, RoundedButton view)
{
#if ANDROID
var drawable = new Android.Graphics.Drawables.GradientDrawable();
drawable.SetCornerRadius(view.CornerRadius);
drawable.SetColor(Android.Graphics.Color.ParseColor("#2196F3"));
handler.PlatformView.SetBackground(drawable);
#elif IOS
handler.PlatformView.Layer.CornerRadius = (nfloat)view.CornerRadius;
handler.PlatformView.BackgroundColor = UIKit.UIColor.SystemBlueColor;
#endif
}
}
// Rejestracja w MauiProgram.cs
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<RoundedButton, RoundedButtonHandler>();
});
Zauważ trzy rzeczy: (1) jeden Handler zamiast osobnego per platformę, (2) preprocesor #if ANDROID/#if IOS zamiast osobnych projektów, (3) rejestracja w MauiProgram.cs zamiast [assembly: ExportRenderer]. Ta trzecia zmiana jest cicha, ale ważna, bo nie musisz już bawić się z atrybutami assembly. Kompilator jasno widzi, co jest zarejestrowane.
Jeśli masz dużo prostych renderers, które tylko zmieniają jedną albo dwie właściwości, rozważ najpierw przepisanie ich na Handlers, a potem (kiedy wszystko już działa) refaktor do prostszych rozwiązań: attached properties, kontrolki natywne w XAML, biblioteka .NET MAUI Community Toolkit. Ja hitowałem ten dokładny błąd w moim ostatnim projekcie migracyjnym, gdzie próbowałem od razu refaktorować i uprościć: skończyło się dwoma tygodniami pełnymi regresji.
DependencyService i MessagingCenter, nowe podejście
DependencyService w Xamarin był w praktyce statycznym service locatorem: antywzorzec, który utrudniał testowanie. MAUI wprost go usunął (wciąż istnieje w namespace, ale jest oznaczony jako [Obsolete]) i zastąpił standardowym kontenerem Microsoft.Extensions.DependencyInjection, tym samym, który znasz z ASP.NET Core. To ogromny zysk. Te same wzorce, ta sama semantyka lifecycle (Singleton/Scoped/Transient), pełna kompatybilność z bibliotekami DI.
// Xamarin.Forms - DependencyService
DependencyService.Register<IDeviceInfo, DeviceInfoImpl>();
var info = DependencyService.Get<IDeviceInfo>();
// .NET MAUI - MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts => fonts.AddFont("OpenSans-Regular.ttf", "OpenSans"));
// Serwisy platformowe
builder.Services.AddSingleton<IDeviceInfo, DeviceInfoImpl>();
// ViewModels
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<MainPage>();
// HttpClient z Refit
builder.Services.AddRefitClient<IApiClient>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
return builder.Build();
}
Analogicznie MessagingCenter (statyczny event bus, przez który dwa niezwiązane ViewModele mogły ze sobą rozmawiać) zastępujemy WeakReferenceMessenger z pakietu CommunityToolkit.Mvvm. Kluczowa zaleta? Słabe referencje eliminują wycieki pamięci, na które w Xamarinie łatwo można było się nadziać. Więcej o wzorcach MVVM z CommunityToolkit znajdziesz w naszym praktycznym przewodniku po MVVM w .NET MAUI.
// Wysyłający ViewModel
public record UserLoggedIn(string UserId);
WeakReferenceMessenger.Default.Send(new UserLoggedIn(userId: "u_42"));
// Nasłuchujący ViewModel
WeakReferenceMessenger.Default.Register<UserLoggedIn>(this, (recipient, message) =>
{
// reaguj na zdarzenie
Debug.WriteLine($"Zalogowano: {message.UserId}");
});
Nawigacja: z NavigationPage do AppShell
W Xamarin.Forms typowa aplikacja używała NavigationPage, TabbedPage i ręcznego PushAsync. Xamarin.Forms Shell dodał deklaratywną alternatywę, ale wiele zespołów zostało przy klasycznej nawigacji. .NET MAUI Shell jest kontynuacją tego kierunku, i w MAUI 10 to zdecydowany domyślny wybór: routing URL, przyciąganie tabów w hierarchie, flyout menu, deep linking, wszystko out of the box.
Deep linking to jedna linijka: Routing.RegisterRoute("details", typeof(DetailsPage)); w konstruktorze AppShell, potem await Shell.Current.GoToAsync($"details?id={itemId}"); zamiast łańcucha PushAsync. Parametr trafia do ViewModelu przez atrybut [QueryProperty]. Migrację warto zrobić stopniowo: najpierw zamień tabbed nav na TabBar, potem zdekomponuj ręczne PushAsync na routing URL.
Migracja platformowa iOS i Android
W MAUI wszystkie projekty platformowe znikają. Zamiast MyApp.Android i MyApp.iOS masz jeden projekt, a kod natywny siedzi w folderach Platforms/Android/ i Platforms/iOS/. Konkretnie:
Android:MainActivity.cs, MainApplication.cs, AndroidManifest.xml w Platforms/Android/. Zwróć uwagę, że FormsAppCompatActivity zniknął. Dziedziczysz teraz po MauiAppCompatActivity.
iOS:AppDelegate.cs, Info.plist, Program.cs (nowy entry point) w Platforms/iOS/. FormsApplicationDelegate zamieniasz na MauiUIApplicationDelegate.
Zasoby: ikony, splash screen i czcionki idą teraz do Resources/ w głównym projekcie. Jeden zestaw dla obu platform, MAUI sam generuje odpowiednie wersje density.
Ważny szczegół z Androida 15: musisz dodać uprawnienia POST_NOTIFICATIONS (jeśli używasz push) i FOREGROUND_SERVICE_* dla nowej granularności usług w tle. iOS wymaga aktualizacji NSPrivacyManifest.xcprivacy. Bez niego App Store Connect odrzuci build od maja 2024. Więcej o publikacji piszemy w przewodniku publikacji .NET MAUI w Google Play i App Store.
Testowanie i publikacja po migracji
Po skompilowaniu projektu MAUI 10 nie odpalaj od razu buildu produkcyjnego. Najpierw uruchom testowo na czterech różnych urządzeniach: fizyczny Android (najniższy wspierany API), emulator Android najnowszy, symulator iOS oraz fizyczny iPhone. Regresje między MAUI 8, 9 i 10 lubią się chować w niuansach. Inaczej działa layout Grid.RowSpan, inaczej focus na TextInput, inaczej gesty w CollectionView.
W moim zespole prowadzimy „go-live checklist”: 40-punktowa lista funkcjonalności, którą tester manualny przechodzi przed każdym releasem po większej migracji. Obejmuje ona logowanie z biometrią, push notifications w foreground/background, głębokie linki, tryb offline, płatności in-app, restart aplikacji po zabiciu procesu i migrację bazy SQLite. Bez takiej checklisty jest niemal pewne, że jeden crash trafi do produkcji (u nas trafił dwa razy, zanim wprowadziliśmy tę procedurę).
Po pomyślnym QA następuje CI/CD. Jeśli w Xamarinie używałeś App Center, teraz musisz przenieść build na GitHub Actions albo Azure Pipelines, bo App Center został wygaszony 31 marca 2025. Standardowa akcja actions/setup-dotnet@v4 z workloadem MAUI działa w 10 minut na standardowym runnerze macOS. Symbolizację crashy przenieś do Sentry albo Firebase Crashlytics; obie mają natywne SDK dla MAUI. Szczegóły podpisywania pakietów i konfiguracji release build znajdziesz w oficjalnej dokumentacji publikacji .NET MAUI, a repo z przykładami migracji jest dostępne na oficjalnym GitHubie .NET MAUI Samples.
Najczęstsze błędy migracji Xamarin do MAUI
Zebrałem je z pięciu ostatnich projektów migracyjnych, w które byłem zaangażowany. Wszystkie są do uniknięcia, jeśli wiesz, na co uważać.
Zapomniana rejestracja Handlera. Custom kontrolka renderuje się jako pusty prostokąt? Sprawdź, czy w MauiProgram.cs masz handlers.AddHandler<YourControl, YourControlHandler>(). To najczęstszy „silent failure”.
Statyczne referencje w DependencyService. Po migracji na DI szukaj wywołań DependencyService.Get<T>() i zamień je na wstrzykiwanie przez konstruktor. Zostawienie ich powoduje NullReference w runtime.
Kolizja namespace’ów. Upgrade Assistant zamienia większość Xamarin.Forms na Microsoft.Maui.Controls, ale nie zawsze poprawnie z Color. Jawne aliasy (using Colors = Microsoft.Maui.Graphics.Colors;) rozwiązują dwuznaczności.
Nieaktualne target frameworks w csproj. Sprawdź, że <TargetFrameworks>net10.0-android;net10.0-ios</TargetFrameworks> jest w każdym relevantnym projekcie. Upgrade Assistant czasem zostawia net8.0.
Ignorowanie warningów o AOT. MAUI 10 z NativeAOT na iOS produkuje szybszy startup, ale reflection-heavy kod (np. serializacja Newtonsoft.Json bez source generators) się wywali. Przejdź na System.Text.Json z source generation.
Pominięcie testów na fizycznym iPhone. Symulator iOS nie łapie problemów z ARM64 AOT, uprawnieniami Face ID czy prawidłowym flowem StoreKit. Zawsze test na sprzęcie (uczyłem się tego na własnej skórze po odrzuconym buildzie z powodu StoreKit).
Często zadawane pytania
Czy Xamarin.Forms jest nadal wspierany w 2026 roku?
Nie. Wsparcie oficjalne Microsoftu dla Xamarin.Forms zakończyło się 1 maja 2024 roku. Aplikacje wciąż działają na urządzeniach, ale nie otrzymują poprawek bezpieczeństwa, nie kompilują się z najnowszymi wersjami Xcode i Google Play zaczyna odrzucać buildy z przestarzałym target SDK.
Jak długo trwa migracja z Xamarin.Forms do .NET MAUI?
Dla małej aplikacji (10–30 ekranów) planujemy 1–2 tygodnie pracy seniora. Średnia aplikacja (50–150 ekranów) to zwykle 4–8 tygodni. Duże projekty z wieloma Custom Renderers i integracjami natywnymi mogą wymagać 3–4 miesięcy. Kluczowa jest liczba renderers i zewnętrznych bindingów, nie liczba ekranów.
Czy mogę migrować część aplikacji stopniowo, mieszając Xamarin i MAUI?
W teorii nie, bo nie da się mieć obu stosów w jednym rozwiązaniu produkcyjnym. W praktyce można użyć techniki „Native Embedding” dostępnej w MAUI 10 do wstrzykiwania pojedynczych stron MAUI do istniejących natywnych aplikacji iOS/Android, ale to inne rozwiązanie niż mieszanie Xamarin.Forms z MAUI. Rekomenduję migrację całościową w feature branchu.
Czym są Handlery w .NET MAUI?
Handler to lekka klasa mostkująca między abstrakcyjną kontrolką MAUI (np. Button) a jej natywną implementacją (Android Button, iOS UIButton). Zastępuje Custom Renderers z Xamarin.Forms, oferując lepszą wydajność (mniej alokacji), prostszą konfigurację przez PropertyMapper i mniej boilerplate. Jeden Handler zamiast osobnego per platforma.
Czy warto migrować, jeśli aplikacja jest w trybie utrzymania?
Tak, i to jest wręcz pilniejsze niż w przypadku aktywnie rozwijanych aplikacji. Aplikacja w utrzymaniu bez migracji przestanie się kompilować przy kolejnej wymianie Xcode i zostanie usunięta z App Store po roku bez aktualizacji. Minimum, jakie musisz zrobić, to migracja MAUI plus jeden release rocznie, żeby aplikacja została w sklepach.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.
Kompletny przewodnik po deep linkach w .NET MAUI 10: konfiguracja Universal Links na iOS, App Links na Androidzie, routing w Shell i najczęstsze błędy weryfikacji AASA oraz assetlinks.json. Z gotowym kodem, przykładami i checklistą do debugowania.
Praktyczny przewodnik po architekturze offline-first w .NET MAUI. Konfiguracja SQLite z EF Core, wykrywanie sieci, trzy wzorce synchronizacji danych i zadania w tle z WorkManager i Shiny — z gotowymi przykładami kodu.
Praktyczny przewodnik po architekturze MVVM w .NET MAUI. CommunityToolkit.Mvvm 8.4 z partial properties, wstrzykiwanie zależności, nawigacja Shell i testy jednostkowe — wszystko, czego potrzebujesz do budowania profesjonalnych aplikacji mobilnych.