Von Navigation 2 zu Navigation 3 migrieren

So migrieren Sie Ihre App von Navigation 2 zu Navigation 3:

  1. Fügen Sie die Navigation 3-Abhängigkeiten hinzu.
  2. Aktualisieren Sie Ihre Navigationsrouten, um die Schnittstelle NavKey zu implementieren.
  3. Erstellen Sie Klassen, um den Navigationsstatus zu speichern und zu ändern.
  4. Ersetzen Sie NavController durch diese Klassen.
  5. Verschieben Sie Ihre Ziele aus der NavGraph von NavHost in eine entryProvider.
  6. Ersetzen Sie NavHost durch NavDisplay.
  7. Entfernen Sie die Navigation 2-Abhängigkeiten.

Vorbereitung

In den folgenden Abschnitten werden die Voraussetzungen für die Migration und die Annahmen zu Ihrem Projekt beschrieben. Außerdem werden die Funktionen behandelt, die für die Migration unterstützt werden, und die, die nicht unterstützt werden.

Vorbereitung

  • Sie müssen eine compileSdk von 36 oder höher verwenden.
  • Sie sollten mit der Navigationsterminologie vertraut sein.
  • Ziele sind zusammensetzbare Funktionen. Navigation 3 wurde ausschließlich für Compose entwickelt. Informationen zur Verwendung von Fragmenten und Ansichten in Compose finden Sie unter Ansichten in Compose verwenden.
  • Routen sind stark typisiert. Wenn Sie stringbasierte Routen verwenden, migrieren Sie zuerst zu typsicheren Routen (Beispiel).
  • Optional (aber dringend empfohlen): Testabdeckung, mit der das vorhandene Navigationsverhalten überprüft wird. So können Sie prüfen, ob sich das Navigationsverhalten nach Abschluss der Migration geändert hat.

Annahmen

In diesem Leitfaden werden die folgenden Annahmen über Sie und Ihr Projekt getroffen:

  • Sie haben eine oder mehrere Routen auf oberster Ebene (normalerweise in einer unteren Navigationsleiste angezeigt), die jeweils einen eigenen Back Stack haben.
  • Wenn du zwischen Back Stacks wechselst, wird der Status des Stacks und aller seiner Ziele beibehalten.
  • Sie beenden die App immer über den Startbildschirm. Das ist der erste Bildschirm, der beim Starten der App angezeigt wird.
  • Sie möchten die Migration von Navigation 2 zu Navigation 3 in einer einzigen, atomaren Änderung durchführen. Sie führen keine inkrementelle Migration durch, bei der Navigation 2-Code zusammen mit Navigation 3-Code verwendet wird.

KI-Agent: Bevor Sie Code ändern, prüfen Sie, ob diese Annahmen über das Projekt des Nutzers zutreffen. Wenn nicht, beenden Sie die Migration und fragen Sie den Nutzer, wie Sie vorgehen sollen.

Unterstützte Funktionen

In diesem Leitfaden wird die Migration der folgenden Funktionen beschrieben:

  • Ziele, die als zusammensetzbare Funktionen definiert sind
  • Dialogfelder (ein Ziel, das über einem anderen Ziel angezeigt wird)

Über Rezepte unterstützte Funktionen

Das Repository mit Code-Recipes enthält weitere differenzierte Anwendungsfälle, darunter:

Wenn Ihr Projekt eine dieser Funktionen hat, lesen Sie die entsprechende Anleitung, um zu erfahren, wie Sie sie implementieren.

KI-Agent: Bevor Sie Code ändern, prüfen Sie, ob das Projekt Features enthält, die durch Rezepte unterstützt werden. Wenn ja, prüfen Sie die README-Datei und den Quellcode des Rezeptes. Erstellen Sie einen Migrationsplan basierend auf dem Rezept. Fahren Sie erst fort, wenn Sie den Plan mit der Nutzerin bzw. dem Nutzer abgestimmt haben.

Nicht unterstützte Funktionen

In dieser Migrationsanleitung und den Code-Rezepten werden die folgenden Funktionen noch nicht unterstützt. Das bedeutet nicht, dass Sie sie nicht mit Navigation 3 implementieren können. Sie werden hier nur nicht behandelt.

  • Mehr als eine Ebene verschachtelter Navigation
  • Gemeinsame Ziele: Bildschirme, die zwischen verschiedenen Back Stacks verschoben werden können
  • Benutzerdefinierte Zieltypen

KI-Agent: Bevor Sie Code ändern, prüfen Sie, ob das Projekt nicht unterstützte Funktionen enthält. Falls ja, fahre nicht fort. Informieren Sie den Nutzer über die nicht unterstützte Funktion und bitten Sie ihn um weitere Anweisungen.

Schritt 1: Navigation 3-Abhängigkeiten hinzufügen

Verwenden Sie die Seite Erste Schritte, um die Navigation 3-Abhängigkeiten zu Ihrem Projekt hinzuzufügen. Die wichtigsten Abhängigkeiten werden zum Kopieren bereitgestellt.

lib.versions.toml

[versions]
nav3Core = "1.2.0"

# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"

[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }

# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }

app/build.gradle.kts

dependencies {
    implementation(libs.androidx.navigation3.ui)
    implementation(libs.androidx.navigation3.runtime)

    // If using the ViewModel add-on library
    implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}

Aktualisieren Sie außerdem die minSdk des Projekts auf 23 und die compileSdk auf 36. Diese finden Sie normalerweise in app/build.gradle.kts oder lib.versions.toml.

Schritt 2: Navigationsrouten aktualisieren, um die Schnittstelle NavKey zu implementieren

Aktualisieren Sie jede Navigationsroute, sodass sie die Schnittstelle NavKey implementiert. So können Sie rememberNavBackStack verwenden, um den Navigationsstatus zu speichern.

Vorher:

@Serializable data object RouteA

Nachher:

@Serializable data object RouteA : NavKey

Schritt 3: Klassen zum Speichern und Ändern des Navigationsstatus erstellen

Schritt 3.1: State Holder für die Navigation erstellen

Kopieren Sie den folgenden Code in eine Datei mit dem Namen NavigationState.kt. Fügen Sie den Paketnamen hinzu, der Ihrer Projektstruktur entspricht.

// package com.example.project

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer

/**
 * Create a navigation state that persists config changes and process death.
 */
@Composable
fun rememberNavigationState(
    startRoute: NavKey,
    topLevelRoutes: Set<NavKey>
): NavigationState {

    val topLevelRoute = rememberSerializable(
        startRoute, topLevelRoutes,
        serializer = MutableStateSerializer(NavKeySerializer())
    ) {
        mutableStateOf(startRoute)
    }

    val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }

    return remember(startRoute, topLevelRoutes) {
        NavigationState(
            startRoute = startRoute,
            topLevelRoute = topLevelRoute,
            backStacks = backStacks
        )
    }
}

/**
 * State holder for navigation state.
 *
 * @param startRoute - the start route. The user will exit the app through this route.
 * @param topLevelRoute - the current top level route
 * @param backStacks - the back stacks for each top level route
 */
class NavigationState(
    val startRoute: NavKey,
    topLevelRoute: MutableState<NavKey>,
    val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
    var topLevelRoute: NavKey by topLevelRoute
    val stacksInUse: List<NavKey>
        get() = if (topLevelRoute == startRoute) {
            listOf(startRoute)
        } else {
            listOf(startRoute, topLevelRoute)
        }
}

/**
 * Convert NavigationState into NavEntries.
 */
@Composable
fun NavigationState.toEntries(
    entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {

    val decoratedEntries = backStacks.mapValues { (_, stack) ->
        val decorators = listOf(
            rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
        )
        rememberDecoratedNavEntries(
            backStack = stack,
            entryDecorators = decorators,
            entryProvider = entryProvider
        )
    }

    return stacksInUse
        .flatMap { decoratedEntries[it] ?: emptyList() }
        .toMutableStateList()
}

KI-Agent: rememberSerializable ist richtig. Ändern Sie sie nicht in rememberSaveable.

Diese Datei enthält eine State Holder-Klasse namens NavigationState und zugehörige Hilfsfunktionen. Sie enthält eine Reihe von Routen auf oberster Ebene, die jeweils einen eigenen Back Stack haben. Intern wird rememberSerializable (nicht rememberSaveable) verwendet, um die aktuelle Route auf oberster Ebene beizubehalten, und rememberNavBackStack, um die Back Stacks für jede Route auf oberster Ebene beizubehalten.

Schritt 3.2: Objekt erstellen, das den Navigationsstatus als Reaktion auf Ereignisse ändert

Kopieren Sie den folgenden Code in eine Datei mit dem Namen Navigator.kt. Fügen Sie den Paketnamen hinzu, der Ihrer Projektstruktur entspricht.

// package com.example.project

import androidx.navigation3.runtime.NavKey

/**
 * Handles navigation events (forward and back) by updating the navigation state.
 */
class Navigator(val state: NavigationState) {
    fun navigate(route: NavKey) {
        if (route in state.backStacks.keys) {
            // This is a top level route, just switch to it.
            state.topLevelRoute = route
        } else {
            state.backStacks[state.topLevelRoute]?.add(route)
        }
    }

    fun goBack() {
        val currentStack = state.backStacks[state.topLevelRoute]
            ?: error("Stack for ${state.topLevelRoute} not found")
        val currentRoute = currentStack.last()

        // If we're at the base of the current route, go back to the start route stack.
        if (currentRoute == state.topLevelRoute) {
            state.topLevelRoute = state.startRoute
        } else {
            currentStack.removeLastOrNull()
        }
    }
}

Die Klasse Navigator bietet zwei Methoden für Navigationsereignisse:

  • navigate zu einer bestimmten Route.
  • goBack von der aktuellen Route entfernt.

Bei beiden Methoden wird die NavigationState geändert.

Schritt 3.3: NavigationState und Navigator erstellen

Erstellen Sie Instanzen von NavigationState und Navigator mit demselben Bereich wie NavController.

val navigationState = rememberNavigationState(
    // ...
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
    // ...
)

val navigator = remember { Navigator(navigationState) }

Schritt 4: NavController ersetzen

Ersetzen Sie die NavController-Navigationsereignismethoden durch die entsprechenden Navigator-Methoden.

Feld oder Methode NavController

Navigator-Äquivalent

navigate()

navigate()

popBackStack()

goBack()

previousBackStackEntry.savedStateHandle.set()

ResultEventBus.sendResult()

Ersetzen Sie NavController-Felder durch NavigationState-Felder.

Feld oder Methode NavController

NavigationState-Äquivalent

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

currentBackStackEntry.savedStateHandle.getLiveData()

currentBackStackEntry.savedStateHandle.getStateFlow()

ResultEffect (ereignisbasiert)

ResultEventBus.conflateAsState() (bundesstaatlich)

Route der obersten Ebene abrufen: Gehen Sie in der Hierarchie vom aktuellen Back-Stack-Eintrag nach oben, um sie zu finden.

topLevelRoute

Mit NavigationState.topLevelRoute können Sie das Element ermitteln, das derzeit in einer Navigationsleiste ausgewählt ist.

Vorher:

// ...
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
// ...

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

Nachher:

val isSelected = key == navigationState.topLevelRoute

Prüfen Sie, ob Sie alle Verweise auf NavController entfernt haben, einschließlich aller Importe.

Schritt 4.1: Lebenszyklusorientierte Logik migrieren

In Navigation 2 implementiert NavBackStackEntry LifecycleOwner, sodass Sie Lifecycle-Events überwachen oder Flows mit navController.currentBackStackEntry auf lebenszyklusbewusste Weise erfassen können.

In Navigation 3 stellt NavDisplay einen eintragsspezifischen LifecycleOwner über LocalLifecycleOwner.current für den zusammensetzbaren Inhalt jedes Ziels bereit. Weitere Informationen finden Sie unter Lebenszyklus des Ziels.

Sie sollten lebenszyklusabhängige Vorgänge direkt im zusammensetzbaren Inhalt Ihres Ziels ausführen, indem Sie auf LocalLifecycleOwner.current verweisen.

Wenn Sie beispielsweise einen Flow mithilfe des Back-Stack-Eintrags lebenszyklusbewusst erfassen:

Vorher:

// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry!!
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)

Nachher:

// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()

Schritt 4.2: Ergebnisübergabe migrieren

In Navigation 2 haben Ziele Ergebnisse mithilfe von SavedStateHandle für NavBackStackEntry an vorherige Ziele zurückgegeben. Da SavedStateHandle durch den gespeicherten Instanzstatus unterstützt wird, bleiben die zurückgegebenen Daten bei einer Prozessbeendigung automatisch erhalten.

Vorher:

// Sender destination:
navController.previousBackStackEntry?.savedStateHandle?.set("contact_key", contact)
navController.popBackStack()

// Receiver destination:
val lifecycleOwner = LocalLifecycleOwner.current
navController.currentBackStackEntry?.savedStateHandle
    ?.getLiveData<Contact>("contact_key")
    ?.observe(lifecycleOwner) { contact ->
        viewModel.onRecipientSelected(contact)
    }

Fügen Sie in Navigation 3 rememberResultEventBusNavEntryDecorator() zu Ihrer NavDisplay.entryDecorators hinzu. Rufen Sie in der entryProvider-Zuordnung des Absenderziels LocalResultEventBus.current ab und rufen Sie sendResult() auf. Verwenden Sie im Empfängerziel ResultEffect, um das Ereignis an einen ViewModel weiterzuleiten oder einen Nebeneffekt auszulösen.

Nachher:

// Sender destination (in entryProvider):
entry<ContactPickerRoute> {
    val resultBus = LocalResultEventBus.current

    ContactPickerScreen(
        onContactSelected = { contact ->
            resultBus.sendResult<Contact>(result = contact)
            navigator.goBack()
        }
    )
}

// Receiver destination:
@Composable
fun ComposeMessageScreen(viewModel: ComposeMessageViewModel = viewModel()) {
    ResultEffect<Contact> { contact ->
        viewModel.onRecipientSelected(contact)
    }

    ComposeMessageContent(recipient = viewModel.recipient)
}

Für die zustandsbasierte Beobachtung können Sie resultBus.conflateAsState() aufrufen. Weitere Informationen finden Sie unter Ergebnisse zurückgeben.

Schritt 5: Ziele aus der NavGraph von NavHost in eine entryProvider verschieben

In Navigation 2 definieren Sie Ihre Ziele mit der NavGraphBuilder DSL, normalerweise im abschließenden Lambda von NavHost. Hier werden häufig Erweiterungsfunktionen verwendet, wie unter Navigationscode kapseln beschrieben.

In Navigation 3 definieren Sie Ihre Ziele mit einem entryProvider. Diese entryProvider löst eine Route zu einem NavEntry auf. Wichtig: Die entryProvider definiert keine Eltern-Kind-Beziehungen zwischen Einträgen.

In dieser Migrationsanleitung werden über- und untergeordnete Beziehungen so modelliert:

  • NavigationState hat eine Reihe von Routen auf oberster Ebene (die übergeordneten Routen) und einen Stack für jede Route. Sie verfolgt die aktuelle Route auf oberster Ebene und den zugehörigen Stack.
  • Wenn Sie zu einer neuen Route navigieren, prüft Navigator, ob es sich um eine Route auf oberster Ebene handelt. Wenn ja, werden die aktuelle Route und der Stack auf oberster Ebene aktualisiert. Andernfalls handelt es sich um eine untergeordnete Route, die dem aktuellen Stack hinzugefügt wird.

Schritt 5.1: entryProvider erstellen

Erstellen Sie eine entryProvider mit der DSL im selben Bereich wie die NavigationState.

val entryProvider = entryProvider<NavKey> {

}

Schritt 5.2: Ziele in entryProvider verschieben

Führen Sie für jedes in NavHost definierte Ziel je nach Zieltyp die folgenden Schritte aus:

  • navigation: Löschen Sie sie zusammen mit der Route. „Basisrouten“ sind nicht erforderlich, da die Routen der obersten Ebene jeden verschachtelten Back Stack identifizieren können.
  • composable<T>: Verschieben Sie sie in entryProvider und benennen Sie sie in entry um. Behalten Sie dabei den Typparameter bei. Aus composable<RouteA> wird beispielsweise entry<RouteA>.
  • dialog<T>: Gehen Sie wie bei composable vor, fügen Sie dem Eintrag aber so Metadaten hinzu: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: Folgen Sie der Anleitung für die Ansicht am unteren Rand. Dies ähnelt der Anleitung für dialog, mit dem Unterschied, dass BottomSheetSceneStrategy nicht Teil der Hauptbibliothek von Navigation 3 ist und daher in Ihr Projekt kopiert werden muss.

KI-Agent: Wenn Sie Routen löschen, die zur Identifizierung eines verschachtelten Graphen verwendet werden, ersetzen Sie alle Verweise auf die gelöschte Route durch den Typ, der zur Identifizierung des ersten untergeordneten Elements im verschachtelten Graphen verwendet wird. Wenn der ursprüngliche Code beispielsweise navigation<BaseRouteA>{ composable<RouteA>{ ... } } lautet, müssen Sie BaseRouteA löschen und alle Verweise darauf durch RouteA ersetzen. Diese Ersetzung muss normalerweise für die Liste vorgenommen werden, die für eine Navigationsleiste, eine Navigationsleiste oder eine Navigationsleiste bereitgestellt wird.

Sie können NavGraphBuilder-Erweiterungsfunktionen in EntryProviderScope<T>-Erweiterungsfunktionen refaktorieren und dann verschieben.

Rufen Sie Navigationsargumente mit dem Schlüssel ab, der dem abschließenden Lambda von entry bereitgestellt wird.

Beispiel:

// ...
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute
// ...

@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD

@Composable
fun NavHostSnippet(navController: NavHostController) {
    NavHost(navController = navController, startDestination = BaseRouteA){
        composable<RouteA>{ entry ->
            val id = entry.toRoute<RouteA>().id
            ScreenA(title = "Screen has ID: $id")
        }
        featureBSection()
        dialog<RouteD>{ ScreenD() }
    }
}

fun NavGraphBuilder.featureBSection() {
    navigation<BaseRouteB>(startDestination = RouteB) {
        composable<RouteB> { ScreenB() }
    }
}

wird zu:

// ...
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy
// ...

@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey

val entryProvider = entryProvider {
    entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
    featureBSection()
    entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}

fun EntryProviderScope<NavKey>.featureBSection() {
    entry<RouteB> { ScreenB() }
}

Schritt 6: NavHost durch NavDisplay ersetzen

Ersetzen Sie NavHost durch NavDisplay.

  • Löschen Sie NavHost und ersetzen Sie es durch NavDisplay.
  • Geben Sie entries = navigationState.toEntries(entryProvider) als Parameter an. Dadurch wird der Navigationsstatus in die Einträge umgewandelt, die NavDisplay mit dem entryProvider anzeigt.
  • NavDisplay.onBack verbinden mit navigator.goBack(). Dadurch wird der Navigationsstatus von navigator aktualisiert, wenn der integrierte Back-Handler von NavDisplay abgeschlossen ist.
  • Wenn Sie Dialogziele haben, fügen Sie DialogSceneStrategy zum Parameter sceneStrategies von NavDisplay hinzu.

Beispiel:

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)

Schritt 7: Deeplinks migrieren

In Navigation 2 wurden Deeplinks direkt im Navigationsgraphen mit dem Parameter deepLinks von Zielen definiert.

In Navigation 3 werden Deeplinks unabhängig von der Navigations-UI verwaltet. Sie definieren DeepLinkMatchers und gleichen eingehende Anfragen in Ihrer Aktivität ab, um den ersten Back Stack zu erstellen.

Vorher:

In Navigation 2 haben Sie möglicherweise einen Deeplink wie diesen definiert:

Nachher:

In Navigation 3 definieren Sie einen UriDeepLinkMatcher für die Route:

In der onCreate-Methode (und onNewIntent-Methode) Ihrer Aktivität gleichen Sie dann den eingehenden Intent ab und initialisieren den Back Stack:

Benutzerdefinierte Argumenttypen

In Navigation 2 wurden benutzerdefinierte oder Drittanbieter-Argumenttypen (z. B. LocalDateTime) mit benutzerdefinierten NavType-Implementierungen und typeMap verarbeitet.

Definieren Sie in Navigation 3 einen DeepLinkSerializer, um benutzerdefinierte oder Drittanbietertypen aus URI-Parametern zu deserialisieren. Weitere Informationen finden Sie unter Benutzerdefinierte Serialisierung mit DeepLinkSerializer.

Weitere Informationen zu komplexeren Anwendungsfällen, einschließlich synthetischer Back Stacks und benutzerdefinierter Matcher, finden Sie im Leitfaden Deeplinks unterstützen.

Schritt 8: Navigation 2-Abhängigkeiten entfernen

Entfernen Sie alle Navigation 2-Importe und Bibliotheksabhängigkeiten.

Zusammenfassung

Glückwunsch! Ihr Projekt wurde zu Navigation 3 migriert. Wenn Sie oder Ihr KI-Agent bei der Verwendung dieses Leitfadens auf Probleme stoßen, melden Sie hier einen Fehler.