.NET MAUI Shell Navigasyon: Rota, Tab ve Flyout Yönetimi Rehberi (2026)
.NET MAUI Shell ile rota tabanlı navigasyon, alt tab, flyout menü ve deep linking'i tek bir AppShell.xaml içinde nasıl kurgularsınız? GoToAsync, IQueryAttributable, DI ve üretim tuzaklarıyla saha odaklı bir rehber.
.NET MAUI Shell, tek bir AppShell.xaml dosyası üzerinden rota tabanlı navigasyon, flyout menü, alt tab çubuğu ve URI stili deep linking sağlayan yüksek seviyeli bir uygulama iskeletidir. Klasik NavigationPage yığınına kıyasla Shell, sayfaları Routing.RegisterRoute ile kayıt ettikten sonra Shell.Current.GoToAsync("//route?param=value") gibi URI çağrılarıyla açmanızı sağlar. Böylece derin bağlantı, geri tuşu davranışı ve modal sunumu tek bir soyutlamada toplanır. Üç MAUI projesinde Shell ile üretime çıktım; bu yazıda mimari kararları ve tuzakları anlatıyorum.
Shell, ShellContent, Tab, FlyoutItem ve TabBar gibi öğelerle bildirimsel bir navigasyon hiyerarşisi tanımlar; kod tarafında yalnızca GoToAsync çağrıları yaparsınız.
Rota parametreleri [QueryProperty] özniteliği veya IQueryAttributable arayüzü ile alınır; karmaşık nesneler için IQueryAttributable daha güvenlidir.
// öneki mutlak (absolute) rota, ../ geri yığın (relative) navigasyon, page ise mevcut yığına ekleme anlamına gelir.
Modal sunum için Shell rotasında Shell.PresentationMode="ModalAnimated" tanımlanır; await Shell.Current.GoToAsync her zaman UI iş parçacığında çağrılmalıdır.
Deep linking, AppLinks (Android) ve Associated Domains (iOS) yapılandırmasıyla birlikte Shell rotalarına doğrudan eşlenir.
Shell ile MVVM entegrasyonu için sayfa ve ViewModel'ler builder.Services.AddTransient ile DI konteynerine kaydedilir; Shell, örnek çözümlemeyi otomatik yapar.
.NET MAUI Shell nedir ve neden kullanılır?
.NET MAUI Shell, Xamarin.Forms Shell'in devamı niteliğinde bir uygulama çerçevesidir. Amacı, uygulamanın üst düzey görsel yapısını (flyout, tab, sayfa hiyerarşisi) XAML üzerinde bildirimsel olarak tanımlamak ve navigasyonu URI benzeri rota dizeleri üzerinden yönetmektir. Bu yaklaşımın en büyük getirisi şu: Ekipteki her geliştirici AppShell.xaml'a bakınca uygulamanın tüm navigasyon topolojisini tek bakışta kavrayabiliyor.
Peki neden Shell tercih ediyorum? Açıkçası klasik NavigationPage ile büyük bir uygulamada 30-40 PushAsync çağrısı yığınına ulaştığınızda geri tuşu davranışı, modal geçişler ve alt tab çubuğu ile üstteki yığın arasındaki ilişki çabucak dağılıyor. Shell bu meseleleri "her sayfanın bir rotası var, o rotaya git" prensibiyle basitleştiriyor. Ayrıca Shell arama çubuğu (SearchHandler), flyout başlık şablonu ve ShellItem.Route ile derin bağlantı desteği kutudan çıktığı gibi gelir.
Shell hiyerarşisi üç ana bileşenden oluşur: FlyoutItem (yan menü öğesi), Tab (alt tab çubuğu bölümü) ve ShellContent (aslında bir sayfaya işaret eden yaprak). Basit bir örnek yapı şöyledir:
FlyoutBehavior üç değer alır: Flyout (varsayılan, yan menü açılır), Locked (menü sabit görünür, tabletler için idealdir) ve Disabled (menü tamamen kapalı, sadece programatik navigasyon). Ürününüzün ana ekranı tabletlerde de kullanılacaksa Locked'ı unutmayın. Bunun üzerinde durmadığım için bir projede tablet düzenini iki hafta gecikmeli teslim etmiştik. Hâlâ canımı sıkar.
ContentTemplate ile DataTemplate kullanmak, sayfaların yalnızca ilk gezinildiğinde oluşturulmasını (lazy init) sağlar. Aksi hâlde uygulama açılışında tüm sayfa nesneleri örneklenir ve MAUI başlangıç süresi ciddi biçimde artar.
Rota kaydı ve GoToAsync ile navigasyon
Shell'de iki tür rota vardır: bildirimsel rotalar (XAML'da ShellContent'e verdiğiniz Route nitelikleri) ve global rotalar (koda Routing.RegisterRoute ile kaydettiğiniz detay sayfaları). Detay sayfaları (örneğin bir liste öğesine tıklayınca açılan makale sayfası) global rotalarla yönetilir. Kayıt genellikle AppShell.xaml.cs içinde yapılır:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Detay sayfaları için global rotalar
Routing.RegisterRoute("article", typeof(ArticleDetailPage));
Routing.RegisterRoute("comments", typeof(CommentsPage));
Routing.RegisterRoute("settings/notifications",
typeof(NotificationSettingsPage));
}
}
Kayıt tamamlandıktan sonra herhangi bir sayfa veya ViewModel içinden şu şekilde navigasyon yapabilirsiniz:
// Yığına ekleme (push)
await Shell.Current.GoToAsync("article?id=42");
// Mutlak rota: yığını temizler ve verilen rotaya gider
await Shell.Current.GoToAsync("//feed");
// Geri git (pop)
await Shell.Current.GoToAsync("..");
// Geri git + parametre gönder
await Shell.Current.GoToAsync("..?refresh=true");
URI eki prensiplerini iyi hatırlayın: // mutlak (root), ../ yığın üstünden çıkma, isim baş öneksiz ise mevcut yığına ekleme anlamına gelir. Bu üçünü karıştırmak, üretimde en sık gördüğüm hatadır. Özellikle "logout" akışında yığında oturum açmış sayfalar kalırsa güvenlik sorununa dönüşür. Çıkış her zaman //login gibi mutlak bir rota ile yapılmalı, tartışmasız.
Sayfalar arasında parametre nasıl geçilir?
Shell'de parametre geçişinin iki resmi yolu vardır. Basit değer türleri için [QueryProperty] özniteliği yeterlidir:
[QueryProperty(nameof(ArticleId), "id")]
public partial class ArticleDetailViewModel : ObservableObject
{
[ObservableProperty]
private int articleId;
partial void OnArticleIdChanged(int value)
{
// Parametre atandığında veriyi yükle
_ = LoadArticleAsync(value);
}
}
Karmaşık nesneler ya da birden çok parametre için IQueryAttributable arayüzü daha güvenlidir, çünkü tüm parametreleri tek atomik çağrıda alırsınız. Bu, iki QueryProperty arasında bir asenkron yükleme tetiklenmesi gibi yarış koşullarını önler:
public partial class OrderDetailViewModel : ObservableObject, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("orderId", out var orderId) &&
query.TryGetValue("customerId", out var customerId))
{
_ = LoadAsync((int)orderId, (int)customerId);
}
}
}
Nesneleri parametre olarak göndermek isterseniz GoToAsync'in IDictionary<string, object> alan aşırı yüklemesini kullanın. URI'ye ToString() sığdırmaya çalışmayın:
var navigationParameter = new Dictionary<string, object>
{
["order"] = selectedOrder,
["customerId"] = currentCustomerId
};
await Shell.Current.GoToAsync("order-detail", navigationParameter);
Modal sunumu ve PresentationMode
Bir sayfayı modal olarak açmak (yani sayfanın alt tab çubuğunu kapatıp kullanıcıyı yalnızca bir göreve odaklamak), Shell'de sayfanın XAML kökünde tek bir öznitelik ile yapılır:
Değerler: NotAnimated, Animated, ModalNotAnimated, ModalAnimated. Modal sunumda tab çubuğu ve flyout otomatik gizlenir. Modal'ı kapatmak için sıradan await Shell.Current.GoToAsync("..") yeterlidir; Shell modal olduğunu bilir.
Deep linking ve URI şeması yapılandırması
Shell rotaları zaten URI benzeri olduğu için deep linking neredeyse ücretsiz gelir. Uygulama dışından (bir bildirimden, e-postadan ya da tarayıcıdan) belirli bir sayfayı açmak için önce platform yapılandırmasını, sonra da Application.Current.SendOnAppLinkRequestReceived köprüsünü kurarsınız.
Android tarafında AndroidManifest.xml'e uygun bir IntentFilter ekleyin:
iOS tarafında Entitlements.plist'e com.apple.developer.associated-domains anahtarını ve sunucu tarafındaki apple-app-site-association dosyasını eklemeniz gerekir. Ardından App.xaml.cs'ye şu köprüyü koyun:
App Links doğrulama protokolünün ayrıntıları için Android App Links resmi kılavuzuna göz atmanızı öneririm. autoVerify="true" için sunucuda assetlinks.json yayınlamadıysanız Android 12 ve sonrasında bağlantı otomatik olarak açılmaz. Bu ayarı unuttuğumuz bir projede kullanıcılar bildirimlerden tıklıyor, tarayıcı açılıyordu; bir gün kaybettik.
Shell mi NavigationPage mi?
Bu, mimari incelemelerimde en sık gelen sorudur. İkisinin de yeri vardır, ama üretimde 6+ sayfalı her yeni MAUI uygulamasını Shell ile başlatıyorum. Karşılaştırma:
Özellik
.NET MAUI Shell
NavigationPage
Navigasyon modeli
URI tabanlı rotalar
Sayfa örneği yığını
Flyout menü
Kutudan çıktığı gibi
Manuel MasterDetailPage gerekir
Alt tab çubuğu
Tab öğesi ile bildirimsel
TabbedPage ile ayrı iskelet
Deep linking
Yerel destek
Manuel yönlendirme kodu
Öğrenme eğrisi
Orta (rota semantiği)
Düşük (Push/Pop)
Uygun senaryo
Çok bölümlü tüketici uygulamaları
2-3 sayfalık dar odaklı akışlar
Modal sunum
PresentationMode özniteliği
PushModalAsync
Kaba kural: Uygulamanız bir "kabuk" (bottom tab veya flyout) etrafında dönüyorsa Shell. Sadece bir sihirbaz akışı ya da onboarding üretiyorsanız düz NavigationPage daha az soyutlamayla iş görür.
Shell içinde Dependency Injection
Shell, sayfa örneklerini oluşturmak için MauiAppBuilder'daki DI konteynerini kullanır. Bu sayede sayfalarınız ve ViewModel'leriniz, kurucu enjeksiyon (constructor injection) ile hizmetleri alabilir. MauiProgram.cs'de kayıt yapmanız yeterlidir:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Hizmetler
builder.Services.AddSingleton<IArticleService, ArticleService>();
builder.Services.AddSingleton<IAuthService, AuthService>();
// Sayfalar ve ViewModel'ler
builder.Services.AddTransient<FeedPage>();
builder.Services.AddTransient<FeedViewModel>();
builder.Services.AddTransient<ArticleDetailPage>();
builder.Services.AddTransient<ArticleDetailViewModel>();
return builder.Build();
}
MVVM tarafını nasıl kurduğumu MAUI MVVM ve CommunityToolkit.Mvvm yazısında ayrıntılı anlatmıştım. Kaynak üreteçlerle birlikte kullanıldığında Shell + ObservableObject + [RelayCommand] üçlüsü, kodun büyük kısmını üretecin yazmasını sağlıyor.
Üretimde karşılaştığım tuzaklar
Şimdi biraz mutfak kısmına inelim. Üç Shell tabanlı uygulamada topladığım deneyimler ışığında dikkat etmeniz gereken beş konu:
Rota kaydını unutmak.Routing.RegisterRoute çağrılmamış bir rotaya GoToAsync yaparsanız çalışma zamanında ArgumentException alırsınız. Testlerinizde bu sayfayı hiç açmıyorsanız hatayı ancak QA sürecinde görürsünüz. Rota isimlerini bir static class Routes altında sabit alanlar olarak tutmayı ve AppShell.xaml.cs'de topluca kaydetmeyi öneririm.
OnAppearing ile veri yükleme yanılgısı. Shell, bir sayfaya geri döndüğünüzde onu bellekte tutar; bu yüzden OnAppearing her odaklanmada çağrılır. Bu, veriyi tekrar tekrar çekmeye yol açabilir. Onun yerine IQueryAttributable içinde tek seferlik yüklemeyi tetikleyin, kullanıcı yenileme isterse RefreshView ile ele alın.
iOS'ta modal + tab çakışması. Bir tab içinden modal açıp modaldan yeni bir tab'e geçmeye çalışırsanız iOS'ta beyaz ekran alırsınız. Modal'ı kapatın, sonra bir Task.Yield() bekleyin, ardından tab değiştirin. Bu sıralamayı bulmam iki gün sürmüştü.
Flyout başlık şablonu bellek sızıntısı.FlyoutHeaderTemplate içinde Image için UriImageSource ile büyük görüntü çekiyorsanız cache stratejisi belirtmediğinizde her Shell yeniden yaratıldığında bellek büyüyor. CachingEnabled="true" ve CacheValidity ayarlayın.
Geri tuşunu geçersiz kılmak. Formda kaydedilmemiş değişiklik varsa BackButtonBehavior ile özel bir komut atayıp DisplayAlert gösterin. Android'in donanım geri tuşu için Shell.BackButtonBehavior, iOS'un swipe-back'i için ayrıca Shell.PresentationMode'u modal olarak ayarlamak veya DisableBackButton ile jesti kesmek gerekir.
Xamarin.Forms'tan geliyorsanız Shell semantiği tanıdık gelecek. Ama .NET MAUI'nin handler mimarisi altta çalıştığı için performans profili farklıdır. MAUI Handler mimarisi yazısında özel bir Shell öğesi render'ını nasıl geçersiz kıldığımızı adım adım anlattım.
Shell SearchHandler ile arama çubuğu eklemek
Shell'in az bilinen ama üretimde çok işime yaramış özelliklerinden biri SearchHandler'dır. Sayfanın başlığına bir arama kutusu koymak ve kullanıcı yazdıkça öneriler göstermek için başka bir kütüphaneye ihtiyaç duymazsınız. SearchHandler'ı özel bir sınıf olarak türetip OnQueryChanged ve OnItemSelected geçersiz kılmalarını doldurursunuz:
Ardından sayfada bağlanır: <Shell.SearchHandler><local:ArticleSearchHandler /></Shell.SearchHandler>. Debounce için Task.Delay ile beklemek ve CancellationToken'ları iptal etmek üretimde şart; her tuş vuruşunda API çağırırsanız hem sunucu hem batarya yanar.
Shell arama çubuğunun tam API yüzeyi ve platform davranış farkları için .NET MAUI SearchHandler dokümantasyonu güncel referans kaynağıdır. iOS'ta SearchHandler'ın UISearchController'a, Android'de ise SearchView'a bağlandığını unutmayın; klavye davranışı ve karakter genişliği platformlar arasında hafifçe değişir.
Sık Sorulan Sorular
.NET MAUI Shell hangi sürümden itibaren üretime hazır?
.NET 8 (Kasım 2023) itibarıyla Shell üretimde kararlıdır. .NET 9 ile flyout başlık şablonu ve tab çubuğu davranışlarında iyileştirmeler geldi. Genel öneri: en az .NET 8 LTS ile başlayın, mümkünse .NET 9 üzerine geçin.
Shell rotasında parametre olarak nesne gönderebilir miyim?
Evet, GoToAsync metodunun IDictionary<string, object> alan aşırı yüklemesi ile nesne referansı gönderebilirsiniz. Alıcı sayfada IQueryAttributable arayüzünü uygulayın. URI'ye ToString() sığdırmaya çalışmayın; sözlük yöntemi hem daha güvenli hem de type-safe.
Shell.Current.GoToAsync neden iOS'ta sessizce başarısız oluyor?
Büyük olasılıkla arka plan iş parçacığından çağırıyorsunuz. Navigasyon UI iş parçacığında gerçekleşmek zorundadır. Çağrıyı MainThread.InvokeOnMainThreadAsync ile sarmalayın veya asenkron zincirinizin en dış katmanında UI'a dönmüş olduğunuzdan emin olun.
Flyout menüsünü belirli sayfalarda gizlemek mümkün mü?
Evet. Sayfa XAML'inde Shell.FlyoutBehavior="Disabled" ve Shell.TabBarIsVisible="False" özniteliklerini kullanabilirsiniz. Onboarding veya ödeme akışı gibi tam ekran deneyimler için idealdir.
Shell içinde birden çok flyout menüsü olabilir mi?
Hayır, uygulamanın yalnızca bir kök Shell'i ve tek bir flyout paneli olabilir. Bunun yerine FlyoutItem öğelerini MenuItem'lar veya grup başlıkları ile bölüp tek bir menüde farklı bölümler oluşturabilirsiniz.
CommunityToolkit.Mvvm 8.4 ve .NET MAUI 9 ile MVVM desenini sıfırdan kurun. ObservableProperty, RelayCommand, WeakReferenceMessenger, Shell navigasyonu ve DI için pratik kod örnekleri ve sık yapılan hatalar.
Blazor Hybrid ile .NET MAUI uygulamalarında web teknolojilerini yerel platform özellikleriyle birleştirin. Proje kurulumundan kimlik doğrulamaya, performans ipuçlarından .NET 11 yeniliklerine kadar kapsamlı rehber.
.NET MAUI handler mimarisinin temellerinden ileri düzey kullanımına kapsamlı bir rehber. PropertyMapper, CommandMapper ile kontrol özelleştirme, sıfırdan özel kontrol oluşturma ve Xamarin renderer geçiş stratejilerini öğrenin.