React Native Widgets für iOS und Android erstellen
AI generated
RN
native
React Native · WidgetKit · Jetpack Glance · Native Module
React Native Widgets
für iOS und Android erstellen

React Native Widgets werden nicht von der JavaScript-Engine gerendert, sondern von den nativen Widget-Frameworks der Plattformen selbst. Dieser Artikel zeigt, wie ein iOS Widget Extension Target mit SwiftUI und ein Android App Widget mit Jetpack Glance sauber an eine bestehende React Native App angebunden werden, inklusive Datenaustausch und Deep-Linking.

20 Min. Lesezeit WidgetKit · Jetpack Glance · App Groups · Deep-Linking React Native 0.74+ · iOS 14+ · Android 12+

1. Warum Widgets nicht Teil der React Native JS-Engine sind

Der häufigste Denkfehler beim Bau von React Native Widgets ist die Annahme, ein Widget sei einfach eine kleinere React-Native-Ansicht. Das Gegenteil ist der Fall: Home-Screen-Widgets laufen in einem eigenen, vom Hauptprozess der App getrennten Prozess und werden ausschließlich von den nativen Widget-Frameworks gerendert, WidgetKit mit SwiftUI auf iOS ab Version 14, Jetpack Glance oder klassische RemoteViews auf Android. Die JavaScript-Engine von React Native, egal ob Hermes oder JSC, wird für das Widget selbst nicht gestartet.

Diese Trennung hat einen guten Grund: Widgets müssen extrem ressourcenschonend sein, da das Betriebssystem sie regelmäßig und ohne Nutzerinteraktion aktualisiert. Ein voller React-Native-Prozess mit JS-Bridge wäre für diesen Update-Zyklus zu schwergewichtig und würde den Akku unnötig belasten. Stattdessen rendert das Betriebssystem eine deklarative View-Beschreibung, die zur passenden Zeit vom System selbst gezeichnet wird, nicht von einer laufenden App-Instanz.

Für ein React Native Projekt bedeutet das konkret: Ein Widget erfordert immer natives Xcode- und Android-Studio-Wissen, zusätzlich zum bestehenden JavaScript-Code. Die Kunst besteht darin, die native Widget-Logik so schlank wie möglich zu halten und alle Daten, die im Widget angezeigt werden sollen, über einen definierten Kanal aus der Haupt-App bereitzustellen, statt Geschäftslogik im Widget selbst zu duplizieren.

2. iOS Widget Extension: Xcode-Target neben der RN-App

Der erste Schritt auf iOS ist ein zusätzliches Xcode-Target vom Typ Widget Extension, das parallel zum bestehenden React-Native-App-Target existiert. Dieses Target hat ein eigenes Info.plist, eigene Swift-Dateien und wird beim Bauen der App als separates Binary mitkompiliert, aber gemeinsam in ein App-Bundle verpackt. Wichtig ist, dass dieses Target dieselbe App Group wie die Haupt-App zugewiesen bekommt, sonst ist später kein Datenaustausch möglich.

Da React-Native-Projekte in der Regel über CocoaPods oder neuerdings auch ohne verwaltet werden, muss das neue Widget-Target in die bestehende Podfile-Konfiguration und in die Xcode-Build-Phasen des RN-Projekts eingebunden werden, ohne die automatisch generierten Dateien von expo prebuild oder react-native init bei jedem Rebuild zu verlieren. Deshalb landet diese Konfiguration entweder in einem eigenen, versionierten Xcode-Projekt-Patch oder in einem Expo Config Plugin, das den Extension-Ordner bei jedem Prebuild automatisch wiederherstellt.


# Typical structure after adding an iOS Widget Extension target
# ios/
#   MyApp.xcodeproj
#   MyApp/                  <- main React Native app target
#   MyAppWidget/             <- new Widget Extension target
#     MyAppWidget.swift
#     Info.plist
#     Assets.xcassets

# Build the app including the widget extension for a device
xcodebuild -workspace ios/MyApp.xcworkspace \
  -scheme MyApp \
  -configuration Release \
  -destination "generic/platform=iOS" \
  build

3. SwiftUI-Widget-View und TimelineProvider

Das Herzstück jedes iOS-Widgets ist der TimelineProvider, der dem System mitteilt, wann welcher Zustand des Widgets angezeigt werden soll. Anders als eine normale App-Ansicht, die auf Nutzerinteraktion reagiert, liefert der TimelineProvider im Voraus eine Zeitleiste von Einträgen, die das System zu den jeweils passenden Zeitpunkten anzeigt, selbst wenn die App gerade nicht läuft. Für Daten, die sich unvorhersehbar ändern, etwa Bestellstatus oder Live-Ereignisse, kombiniert man das mit einem expliziten Reload-Trigger aus der App heraus.

Die eigentliche SwiftUI-View des Widgets bleibt bewusst simpel: Sie liest ausschließlich aus dem TimelineEntry, den der Provider bereitstellt, und enthält keine eigene Netzwerklogik. Das hält das Widget schnell renderbar und verhindert, dass ein hängender Netzwerkaufruf die Widget-Aktualisierung blockiert, was vom System ohnehin nach einer knappen Zeitbudget-Grenze abgebrochen würde.


import WidgetKit
import SwiftUI

struct OrderStatusEntry: TimelineEntry {
    let date: Date
    let statusText: String
}

struct OrderStatusProvider: TimelineProvider {
    func placeholder(in context: Context) -> OrderStatusEntry {
        OrderStatusEntry(date: Date(), statusText: "Loading...")
    }

    func getSnapshot(in context: Context, completion: @escaping (OrderStatusEntry) -> Void) {
        completion(OrderStatusEntry(date: Date(), statusText: readSharedStatus()))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<OrderStatusEntry>) -> Void) {
        let entry = OrderStatusEntry(date: Date(), statusText: readSharedStatus())
        // Refresh again in 30 minutes at the latest
        let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
        completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
    }
}

struct OrderStatusWidgetView: View {
    var entry: OrderStatusProvider.Entry

    var body: some View {
        Text(entry.statusText)
            .font(.headline)
            .widgetURL(URL(string: "myapp://orders/latest"))
    }
}

4. Android App Widget mit Jetpack Glance

Auf Android bilden ein AppWidgetProvider und eine Deklaration in AndroidManifest.xml den Rahmen jedes App Widgets. Klassisch wurden Widgets über RemoteViews und XML-Layouts gebaut, ein umständlicher Prozess, da RemoteViews nur eine stark eingeschränkte Teilmenge von Android-Views unterstützt. Jetpack Glance löst dieses Problem, indem es einen Compose-ähnlichen, deklarativen API-Stil bereitstellt, der intern trotzdem zu RemoteViews kompiliert wird, aber deutlich angenehmer zu schreiben ist.

Der AppWidgetProvider übernimmt den Lebenszyklus: Er wird beim Hinzufügen, Aktualisieren und Entfernen des Widgets vom System aufgerufen und liest dabei, ähnlich wie der iOS-TimelineProvider, ausschließlich aus einem bereits vorbereiteten Datenspeicher, nicht aus einem Live-Netzwerkaufruf. Die Update-Frequenz wird über android:updatePeriodMillis im Widget-Provider-XML konfiguriert, wobei das System aus Batterie-Gründen ein Minimum von etwa dreißig Minuten durchsetzt, kürzere Intervalle erfordern einen expliziten Trigger aus der App.


import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.text.Text
import androidx.glance.action.clickable
import androidx.glance.action.actionStartActivity

class OrderStatusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            val status = readSharedStatus(context)
            Text(
                text = status,
                modifier = GlanceModifier.clickable(
                    actionStartActivity<MainActivity>(
                        // Deep link back into the app's order screen
                        actionParametersOf(deepLinkKey to "myapp://orders/latest")
                    )
                )
            )
        }
    }
}

class OrderStatusWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = OrderStatusWidget()
}

5. Datenaustausch: App Groups und SharedPreferences

Da Widget-Prozess und App-Prozess getrennt laufen, benötigen React Native Widgets einen expliziten Kanal, um Daten auszutauschen. Auf iOS übernehmen App Groups diese Aufgabe: Sowohl das Haupt-App-Target als auch das Widget-Extension-Target werden derselben App Group zugewiesen, wodurch beide auf einen gemeinsamen UserDefaults(suiteName:)-Bereich oder einen gemeinsamen Datei-Container zugreifen können. Die React-Native-Seite schreibt über ein natives Modul in diesen gemeinsamen Speicher, das Widget liest von dort beim nächsten Timeline-Refresh.

Auf Android übernimmt entweder ein geteilter SharedPreferences-Namespace oder, für strukturiertere Daten, ein eigener Content Provider dieselbe Rolle. Wichtig bei beiden Plattformen: Die im Widget angezeigten Daten sollten möglichst klein und bereits fertig aufbereitet sein, etwa ein formatierter Statustext statt eines rohen JSON-Objekts, damit das Widget selbst keine zusätzliche Parsing- oder Formatierungslogik benötigt und schnell rendert.

6. Expo, Config Plugins und Custom Dev Client

Ein reines Expo-Managed-Projekt kann keine Widgets enthalten, da diese natives Xcode- und Android-Studio-Projektwissen voraussetzen, das der Standard-Expo-Go-Client nicht mitbringt. Die Lösung ist ein Expo Config Plugin, das beim expo prebuild-Lauf die Widget-Extension-Dateien und die nötigen Xcode-Projektänderungen automatisch generiert, kombiniert mit einem Custom Dev Client, der diese native Erweiterung tatsächlich enthält.

Bibliotheken wie react-native-android-widget reduzieren zusätzlich den Kotlin-Boilerplate, indem sie erlauben, das Widget-Layout direkt aus React-Native-JSX zu beschreiben, das zur Build-Zeit in native Views übersetzt wird. Das funktioniert für einfache, statische Layouts gut, ersetzt aber nicht die Notwendigkeit, App Groups, TimelineProvider oder AppWidgetProvider grundsätzlich zu verstehen, sobald komplexere Update-Logik gefragt ist.


{
  "expo": {
    "plugins": [
      [
        "./plugins/withIosWidget",
        { "appGroupIdentifier": "group.com.example.myapp" }
      ],
      [
        "react-native-android-widget",
        { "widgets": [{ "name": "OrderStatusWidget", "minWidth": "150dp" }] }
      ]
    ]
  }
}

7. Widget-Refresh aus JavaScript auslösen

Sobald sich relevante Daten in der App ändern, etwa ein neuer Bestellstatus eintrifft, soll das Widget diese Änderung möglichst zeitnah zeigen, statt auf den nächsten automatischen Update-Zyklus zu warten. Auf iOS geschieht das über WidgetCenter.shared.reloadAllTimelines(), aufgerufen aus einem nativen Modul, das von JavaScript per Bridge angesprochen wird. Diese Methode fordert beim System sofort einen neuen Timeline-Eintrag an, statt auf das nächste natürliche Update-Fenster zu warten.

Auf Android sendet die App einen expliziten Broadcast an den AppWidgetManager, der wiederum den AppWidgetProvider zum sofortigen Neuzeichnen anstößt. Beide Mechanismen sollten sparsam eingesetzt werden: Ein Refresh bei jeder einzelnen App-Öffnung ist unnötig, sinnvoll ist ein Trigger genau dann, wenn sich die im Widget angezeigten Daten tatsächlich geändert haben, etwa nach einem erfolgreichen Netzwerk-Sync im Hintergrund.


// widgetBridge.ts — trigger native widget refresh from JS after data changes
import { NativeModules, Platform } from 'react-native';

const { WidgetBridge } = NativeModules;

export async function syncOrderStatusToWidget(statusText: string) {
  // Write the formatted status into the shared storage (App Group / SharedPreferences)
  await WidgetBridge.writeSharedStatus(statusText);

  // Ask the OS to redraw the widget as soon as possible
  if (Platform.OS === 'ios') {
    WidgetBridge.reloadAllTimelines();
  } else {
    WidgetBridge.requestWidgetUpdate();
  }
}

Ein Widget, auf das man tippen kann, ohne dass etwas passiert, verfehlt einen Großteil seines Zwecks. Auf iOS wird das über widgetURL() in der SwiftUI-View gelöst, das dem gesamten Widget oder einzelnen Bereichen eine URL zuweist, die beim Tap über das URL-Scheme oder Universal Links der App geöffnet wird. Auf Android übernimmt actionStartActivity in Kombination mit einem Intent, der dieselbe URL als Deep Link an die Haupt-Activity übergibt, dieselbe Rolle.

Wichtig ist, dass diese Deep-Link-URLs exakt dieselben Routen ansprechen, die auch im regulären App-Navigationssystem existieren, etwa über React Navigation oder Expo Router konfiguriert. So landet ein Nutzer nach dem Tap auf das Widget direkt auf der relevanten Bestell- oder Detailansicht, statt auf dem Startbildschirm der App, von dem aus er erneut navigieren müsste.

9. WidgetKit und Jetpack Glance im Vergleich

Auch wenn beide Plattformen konzeptionell ähnliche Ziele verfolgen, unterscheiden sich WidgetKit und Jetpack Glance in wichtigen technischen Details, die den Implementierungsaufwand für React Native Widgets direkt beeinflussen.

Dimension iOS WidgetKit Android Jetpack Glance
Rendering-Technologie SwiftUI-Views über TimelineProvider Compose-ähnliches API, kompiliert zu RemoteViews
Update-Mechanismus Timeline mit geplanten Einträgen, plus manueller Reload updatePeriodMillis plus manueller Broadcast
Datenaustausch mit der App App Groups mit gemeinsamem UserDefaults SharedPreferences oder eigener Content Provider
Deep-Linking widgetURL() pro View oder Bereich actionStartActivity mit Intent-Extras
Mindestversion iOS 14 für WidgetKit, iOS 17 für interaktive Widgets Android 12 für vollen Material-You-Support in Glance

Für ein React-Native-Team bedeutet diese Gegenüberstellung vor allem: Der native Code für iOS und Android bleibt vollständig getrennt, es gibt keine gemeinsame Widget-Codebasis über beide Plattformen hinweg. Was geteilt werden kann, ist ausschließlich die Datenaufbereitung auf der App-Seite, die dann über plattformspezifische Brücken in die jeweilige native Widget-Implementierung fließt.

Mironsoft

React Native Entwicklung, native Erweiterungen und plattformspezifische Integrationen

Home-Screen-Widgets für eure React Native App?

Wir bauen iOS Widget Extensions mit SwiftUI und Android App Widgets mit Jetpack Glance, sauber angebunden an eure bestehende React Native App über App Groups, Deep-Linking und automatisierte Refresh-Trigger.

iOS WidgetKit

SwiftUI-Widgets mit TimelineProvider und App-Group-Anbindung

Android Glance

App Widgets mit Jetpack Glance und zuverlässigem Refresh

Expo-Integration

Config Plugins und Custom Dev Client für Widget-Support

10. Zusammenfassung

React Native Widgets entstehen nicht in JavaScript, sondern in den nativen Widget-Frameworks jeder Plattform: WidgetKit mit SwiftUI und einem TimelineProvider auf iOS, Jetpack Glance mit einem AppWidgetProvider auf Android. Beide Welten benötigen einen eigenen Xcode- beziehungsweise Android-Studio-Build-Prozess, der über App Groups und SharedPreferences mit der React-Native-Hauptanwendung Daten austauscht, statt eigene Netzwerklogik im Widget selbst zu betreiben.

Für Expo-Projekte sind Config Plugins und ein Custom Dev Client der pragmatische Weg, um native Widget-Erweiterungen ohne vollständigen Ausstieg aus dem Expo-Workflow zu integrieren. Der entscheidende Erfolgsfaktor bleibt aber immer derselbe: schlanke, bereits fertig aufbereitete Daten im Widget selbst, sinnvolles Refresh-Triggering aus der App heraus, und funktionierendes Deep-Linking zurück in die richtige Ansicht der Anwendung.

React Native Widgets — Das Wichtigste auf einen Blick

Getrennter Prozess

Widgets laufen ohne React-Native-JS-Engine, gerendert von WidgetKit oder Jetpack Glance direkt.

Datenaustausch

App Groups auf iOS, SharedPreferences oder Content Provider auf Android, für kleine, fertige Daten.

Refresh-Trigger

WidgetCenter.reloadAllTimelines und AppWidgetManager-Broadcast statt Dauerpolling.

Deep-Linking

widgetURL und Intent-Extras führen zur exakt richtigen Ansicht in der App.

11. FAQ: React Native Widgets für iOS und Android

1Widget komplett in JavaScript schreiben?
Nein, gerendert wird von WidgetKit oder Jetpack Glance, nicht von der React-Native-JS-Engine.
2Funktionieren Widgets mit Expo Go?
Nein, nötig sind ein Config Plugin und ein Custom Dev Client mit der Widget-Extension.
3Was macht ein TimelineProvider?
Er liefert im Voraus eine Zeitleiste von Einträgen, die zu passenden Zeitpunkten angezeigt werden.
4Wie tauschen Widget und App Daten aus?
App Groups auf iOS, SharedPreferences oder Content Provider auf Android.
5Wie oft kann sich ein Widget aktualisieren?
Minimum etwa dreißig Minuten automatisch, sofortiges Update über manuellen Reload-Trigger.
6Was ist Jetpack Glance?
Ein Compose-ähnliches API, das intern zu RemoteViews kompiliert wird, deutlich angenehmer als klassisches XML.
7Wie funktioniert Deep-Linking aus dem Widget?
widgetURL auf iOS, actionStartActivity mit Intent-Extras auf Android.
8Netzwerkaufrufe direkt im Widget?
Nein, das System bricht hängende Aufrufe ab. Daten sollten vorher fertig aufbereitet im Speicher liegen.
9Ab welcher Version funktionieren Widgets?
WidgetKit ab iOS 14, interaktive Widgets ab iOS 17. Jetpack Glance ab Android 8, voller Support ab Android 12.
10Kann ich Widget-Code teilen?
Nur die Datenaufbereitung, der native Rendering-Code bleibt plattformspezifisch getrennt.