.NET MAUI 10 Home Screen Widgets: iOS WidgetKit and Android Glance Guide (2026)

Home screen widgets in .NET MAUI 10 need native extensions per platform. Here is the exact iOS WidgetKit and Android Glance setup, with shared data via App Groups, deep linking through Shell, and working code samples I use in production.

.NET MAUI 10 Widgets: iOS & Android Guide (2026)

Updated: June 10, 2026

Home screen widgets in .NET MAUI 10 require native extensions built per platform. There is no cross-platform widget API yet, so you ship a Swift WidgetKit extension for iOS and a Kotlin Glance widget for Android, then bridge them to your MAUI app through App Groups (iOS) and shared preferences or content providers (Android). This guide shows the exact project structure, code, and data-sharing pattern I use in production MAUI 10 apps to put live, interactive widgets on iOS 18, iOS 26, Android 14, and Android 16 home screens.

  • .NET MAUI 10 cannot author widgets in C# alone. Each platform needs its own native extension (Swift/SwiftUI for iOS, Kotlin/Glance for Android).
  • iOS uses WidgetKit with a separate App Extension target; share data through App Groups and a shared UserDefaults suite.
  • Android 12+ widgets are built with Jetpack Glance (Compose-based) and registered through an AppWidgetProvider in the host manifest.
  • Widget timelines on iOS refresh on a system-controlled budget (roughly 40 to 70 times per day); Android Glance refreshes via WorkManager or AlarmManager.
  • Deep linking from a widget to a specific MAUI page uses URL schemes or Universal/App Links. The same Shell routes you already use for push notifications work here.
  • Lock-screen widgets (iOS) and Material You dynamic colors (Android) require small per-platform tweaks but reuse the same data layer.

Can .NET MAUI create home screen widgets?

Not directly. .NET MAUI 10 still has no built-in API for home screen widgets, and the .NET runtime cannot execute inside an iOS WidgetKit extension or an Android Glance widget process. Both platforms place strict memory and execution-time limits on widget code (iOS extensions get about 30 MB, Android Glance runs in a remote process), and the Mono runtime simply cannot start within those constraints. So in every shipping MAUI app I've built, the widget itself is written in native code: SwiftUI on iOS, Kotlin with Jetpack Glance on Android. The MAUI side's job is to produce the data the widget consumes (quotes, step counts, the next calendar event) and persist it somewhere both processes can read.

Honestly, that shared-state contract is the whole architecture. Get it right and adding new widgets becomes a templating exercise. If you already use Shell routing and dependency injection in your MAUI app, see our deep dive on navigation and DI for the patterns I assume below.

Adding an iOS WidgetKit extension to a .NET MAUI 10 app

A WidgetKit extension is a separate Xcode target that gets bundled inside your iOS .app. With .NET MAUI 10 you cannot add this target from Visual Studio directly, but the project system now supports referencing pre-built native extensions through the NativeReference item with Kind="AppExtension". The workflow I use:

  1. Open an empty Xcode project, add a Widget Extension target named, for example, QuotesWidget.
  2. Write the widget in SwiftUI (see snippet below), set its deployment target to match your MAUI app's SupportedOSPlatformVersion.
  3. Build it as a .appex bundle, drop it into a Platforms/iOS/Extensions/ folder in your MAUI project.
  4. Reference it from your .csproj so the .NET for iOS linker copies it into the final IPA.
<ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  <NativeReference Include="Platforms/iOS/Extensions/QuotesWidget.appex">
    <Kind>AppExtension</Kind>
  </NativeReference>
</ItemGroup>

The SwiftUI widget itself is unremarkable WidgetKit code. The interesting part is how it reads the data your MAUI app wrote:

import WidgetKit
import SwiftUI

struct QuoteEntry: TimelineEntry {
    let date: Date
    let text: String
    let author: String
}

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> QuoteEntry {
        QuoteEntry(date: Date(), text: "Loading...", author: "")
    }
    func getSnapshot(in context: Context, completion: @escaping (QuoteEntry) -> Void) {
        completion(currentEntry())
    }
    func getTimeline(in context: Context, completion: @escaping (Timeline<QuoteEntry>) -> Void) {
        let next = Date().addingTimeInterval(60 * 30)
        completion(Timeline(entries: [currentEntry()], policy: .after(next)))
    }
    private func currentEntry() -> QuoteEntry {
        let defaults = UserDefaults(suiteName: "group.com.mobiletechlead.quotes")
        return QuoteEntry(
            date: Date(),
            text: defaults?.string(forKey: "quote_text") ?? "...",
            author: defaults?.string(forKey: "quote_author") ?? "")
    }
}

How to share data between a MAUI app and an iOS widget

Both the MAUI app and the WidgetKit extension must declare the same App Group capability and use a UserDefaults suite (or a file inside the group container). I recommend creating a small abstraction in your MAUI iOS partial class so the rest of the app stays clean:

// Platforms/iOS/SharedStorage.iOS.cs
using Foundation;

public partial class SharedStorage : ISharedStorage
{
    private const string SuiteName = "group.com.mobiletechlead.quotes";
    private readonly NSUserDefaults _defaults = new(SuiteName, NSUserDefaultsType.SuiteName);

    public void Set(string key, string value)
    {
        _defaults.SetString(value, key);
        _defaults.Synchronize();
        // Tell the widget to reload its timeline.
        WidgetKit.WidgetCenter.SharedCenter.ReloadAllTimelines();
    }
}

Add the matching entitlement to Platforms/iOS/Entitlements.plist:

<key>com.apple.security.application-groups</key>
<array>
  <string>group.com.mobiletechlead.quotes</string>
</array>

The same App Group ID must be enabled on the WidgetKit extension's provisioning profile. If you ship through a pipeline, our walkthrough on code signing MAUI apps in CI/CD covers exporting the right entitlements file alongside the widget's .appex.

Building an Android home screen widget with Glance

Android 12 introduced Jetpack Glance, a Compose-based DSL for AppWidgets that replaces the older RemoteViews XML approach. As of the Glance 1.1.0 release line, it's the official recommendation from Google. See the Glance documentation for the full API surface, and the official AppWidget samples on GitHub for working reference implementations. The integration with a .NET MAUI 10 app is similar to iOS: the widget lives in a small Kotlin project, compiled to an AAR, then referenced as an AndroidLibrary.

The Glance widget itself uses standard composables and reads from the same shared preferences your MAUI app writes:

class QuotesWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val prefs = context.getSharedPreferences("quotes_shared", Context.MODE_PRIVATE)
        val text = prefs.getString("quote_text", "...") ?: "..."
        val author = prefs.getString("quote_author", "") ?: ""
        provideContent {
            Column(GlanceModifier.padding(12.dp).background(GlanceTheme.colors.surface)) {
                Text(text, style = TextStyle(fontSize = 16.sp))
                Spacer(GlanceModifier.height(4.dp))
                Text("by $author", style = TextStyle(fontSize = 12.sp))
            }
        }
    }
}

class QuotesWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = QuotesWidget()
}

On the MAUI side, the C# code that updates the widget data and triggers a refresh is short:

// Platforms/Android/SharedStorage.Android.cs
using Android.Content;

public partial class SharedStorage : ISharedStorage
{
    public void Set(string key, string value)
    {
        var ctx = Platform.AppContext;
        var prefs = ctx.GetSharedPreferences("quotes_shared", FileCreationMode.Private);
        prefs!.Edit()!.PutString(key, value)!.Apply();

        // Ask Glance to refresh every instance of the widget on the home screen.
        // The static updateAll helper is reached through a small Kotlin facade
        // exposed by the widget AAR so we keep Glance internals out of MAUI.
        WidgetBridge.RefreshAll(ctx);
    }
}

Register the widget in AndroidManifest.xml with an intent filter and a metadata XML pointing at preview, sizing, and update period.

How often do widgets refresh and how to control it

This is the single most-asked question I see in MAUI community channels, and the answer differs sharply by platform.

iOS: WidgetKit refreshes are budgeted by the system, not the developer. Apple's guidance is that an actively-used widget receives around 40 to 70 timeline-provider invocations per day, with the actual number tuned by usage signals. You influence this through the TimelinePolicy: .after(date), .atEnd, or .never. The official WidgetKit refresh guide documents the budget rules in detail. For data that changes on a user action (a toggle, a save, a workout finish), call WidgetCenter.ReloadAllTimelines() from inside your MAUI app. That's a free, immediate refresh that does not consume the daily budget.

Android: The legacy updatePeriodMillis field is capped at 30 minutes minimum and is unreliable on Doze-mode devices. For Glance, the modern pattern is to schedule a WorkManager task from your MAUI app whenever data changes, and have that worker call QuotesWidget().updateAll(context). WorkManager respects battery and network constraints, which is what Play Store reviewers expect in 2026.

Refresh mechanismiOS WidgetKitAndroid Glance
System-driven refresh40 to 70 / day budgetMin 30 min via updatePeriodMillis
App-driven refreshWidgetCenter.ReloadAllTimelines()WorkManager → updateAll()
Event-driven (push)Background push hintFCM → enqueueUniqueWork
User-interactiveAppIntent (iOS 17+)actionRunCallback
Lock screen supportYes, iOS 16+No (separate lockscreen APIs)

Deep linking from a widget into a MAUI page

Tapping a widget should land the user on a relevant page in your MAUI app, not just the launcher screen. The mechanism is the same one you already use for push notifications: a URL passed to the OS, intercepted by your App startup code, and routed through Shell. If you haven't wired Shell deep links yet, see our complete deep-linking guide for .NET MAUI 10.

On iOS, attach a widgetURL modifier in SwiftUI:

Text(quote.text)
    .widgetURL(URL(string: "mobiletechlead://quote/\(quote.id)"))

On Android, build a PendingIntent to your MainActivity with the same URL, and attach it via actionStartActivity in Glance. Inside the MAUI app, override OpenUrl on the lifecycle and call Shell.Current.GoToAsync. Because both platforms converge on a URL string, a single C# router can decide which Shell route to navigate to, keeping the widget UX consistent across iOS and Android without duplicating logic.

Lock screen widgets and Material You dynamic colors

iOS 16 added accessory widgets that live on the lock screen and the always-on display, and iOS 18 expanded them with tinted modes. They use the same WidgetKit project. You just declare additional WidgetFamily support (.accessoryCircular, .accessoryRectangular, .accessoryInline). The rendering palette is restricted to monochrome with vibrant tinting, so design for that constraint from the start. Lock-screen widgets pair naturally with Live Activities and the Dynamic Island, since both surfaces share entitlements and frequently reuse the same data model.

On Android, Material You dynamic colors give widgets a theme that matches the user's wallpaper-derived palette. Use GlanceTheme.colors directly in your composables. Glance maps these to the system colorScheme tokens automatically on Android 12 and above, and falls back to a default theme on older versions. Avoid hard-coded hex colors in widget UIs, or you'll get user complaints about jarring contrast on devices running Material You themes.

Testing and debugging widgets

Widgets are notoriously hard to test because the host process is owned by the OS launcher, not your app. The techniques that have saved me hours of guesswork on real MAUI projects:

  • iOS: Run the widget target directly in Xcode by selecting the QuotesWidget scheme. The simulator will install it as a stand-alone preview and you can debug the SwiftUI rendering without rebuilding the MAUI app.
  • Android: Use adb shell am broadcast -a android.appwidget.action.APPWIDGET_UPDATE to force a refresh, and add temporary log statements inside provideGlance. Glance forwards them through Logcat with the tag GlanceAppWidget.
  • Shared data: Inspect the iOS App Group container with xcrun simctl get_app_container booted com.mobiletechlead.quotes data, and the Android shared preferences with adb shell run-as com.mobiletechlead.quotes cat shared_prefs/quotes_shared.xml.
  • Snapshot tests: WidgetKit ships with WidgetPreviewContext for SwiftUI previews; Glance has GlanceAppWidgetReceiverTest. Run them in your platform CI matrix alongside your MAUI UI automation suite to catch layout regressions early.

One last operational note: when you push a widget update through TestFlight or Play Console internal testing, the widget on a tester's device will not reload automatically until the host app is launched once. Document this in your tester instructions, otherwise you'll get false bug reports about "the widget still shows old data."

Frequently Asked Questions

Does .NET MAUI 10 support iOS and Android widgets out of the box?

No. There is no cross-platform widget API in MAUI 10. You must add a native WidgetKit extension on iOS and a Glance/AppWidget on Android, then share data through App Groups and shared preferences respectively.

Can I write a widget in C# instead of Swift or Kotlin?

Not realistically. The .NET runtime cannot start inside the strict memory and time budget the OS gives a widget process. Keep the widget UI in SwiftUI or Glance and let your MAUI app produce the data.

How do I force an iOS widget to refresh immediately?

From your MAUI app, call WidgetKit.WidgetCenter.SharedCenter.ReloadAllTimelines() right after you write new data. This bypasses the daily timeline budget for that one update.

Why does my Android widget stop updating after a few hours?

Doze mode and the system's adaptive battery throttling defer updatePeriodMillis. Switch to WorkManager-driven updates: schedule a worker when data changes, and call QuotesWidget().updateAll(context) from inside it.

Do interactive widgets work in .NET MAUI 10?

Yes. iOS 17 added AppIntent-backed buttons that run code without launching the app, and Android Glance supports actionRunCallback. Both call back into a small native handler. You cannot run MAUI code on the widget thread, but you can queue work that your app handles next time it wakes.

Editorial Team
About the Author Editorial Team

Our team of expert writers and editors.