מעבר מ-Navigation 2 ל-Navigation 3

כדי להעביר את האפליקציה מ-Navigation 2 ל-Navigation 3, פועלים לפי השלבים הבאים:

  1. מוסיפים את יחסי התלות של Navigation 3.
  2. מעדכנים את מסלולי הניווט כדי להטמיע את הממשק NavKey.
  3. יוצרים מחלקות כדי לשמור ולשנות את מצב הניווט.
  4. מחליפים את NavController בסוגים האלה.
  5. מעבירים את היעדים מNavGraph של NavHost אל entryProvider.
  6. מחליפים את NavHost ב-NavDisplay.
  7. מסירים את יחסי התלות של Navigation 2.

הכנה

בקטעים הבאים מתוארים התנאים המוקדמים להעברה וההנחות לגבי הפרויקט. הם גם מפרטים את התכונות שנתמכות בהעברה ואת אלה שלא נתמכות.

דרישות מוקדמות

  • צריך להשתמש ב-compileSdk בגרסה 36 ואילך.
  • אתם צריכים להכיר את המונחים שקשורים לניווט.
  • היעדים הם פונקציות קומפוזביליות. ‫Navigation 3 מיועדת אך ורק ל-Compose. כדי להשתמש במקטעים (fragments) ובתצוגות (Views) בפיתוח נייטיב, קראו את המאמר שימוש בתצוגות בפיתוח נייטיב.
  • המסלולים מוגדרים באופן קשיח. אם אתם משתמשים בנתיבים שמבוססים על מחרוזות, קודם צריך לעבור לנתיבים עם מניעת שגיאות הקלדה (דוגמה).
  • אופציונלי (אבל מומלץ מאוד): כיסוי בדיקה שמאמת את התנהגות הניווט הקיימת. כך אפשר לוודא שהתנהגות הניווט לא השתנתה אחרי שההעברה הושלמה.

הנחות

המדריך הזה מבוסס על ההנחות הבאות לגביכם ולגבי הפרויקט שלכם:

  • יש לכם נתיב אחד או כמה נתיבים ברמה העליונה (בדרך כלל מוצגים בסרגל ניווט תחתון), ולכל אחד מהם יש מקבץ פעילויות קודמות משלו.
  • כשעוברים בין מקבצי פעילויות קודמות, המצב של המקבץ וכל היעדים שלו נשמרים.
  • כדי לצאת מהאפליקציה, צריך לעבור למסך הבית, שהוא המסך הראשון שמוצג כשמפעילים את האפליקציה.
  • אתם רוצים לבצע את המעבר מ-Navigation 2 ל-Navigation 3 בשינוי אטומי אחד. אתם לא מבצעים העברה הדרגתית שבה קוד של Navigation 2 משמש לצד קוד של Navigation 3.

‫סוכן AI: לפני שמשנים את הקוד, צריך לבדוק אם ההנחות האלה לגבי הפרויקט של המשתמש נכונות. אם לא, צריך לעצור את ההעברה ולשאול את המשתמש איך להמשיך.

תכונות נתמכות

המדריך הזה מאפשר לכם להעביר את התכונות הבאות:

  • יעדים שמוגדרים כפונקציות קומפוזביליות
  • תיבות דו-שיח (יעד שמוצג מעל יעד אחר)

תכונות שנתמכות באמצעות מתכונים

מאגר המתכונים של הקוד מדגים תרחישים מורכבים יותר, כולל:

אם בפרויקט שלכם יש אחת מהתכונות האלה, כדאי לבדוק את המתכון הרלוונטי כדי להבין איך להטמיע אותן.

סוכן AI: לפני שמשנים קוד, בודקים אם הפרויקט מכיל תכונות שנתמכות באמצעות מתכונים. אם כן, צריך לבדוק את קובץ ה-README ואת קוד המקור של המתכון. יוצרים תוכנית העברה על סמך המתכון. אל תמשיכו בלי לאשר את התוכנית עם המשתמש.

תכונות שאינן נתמכות

המדריך הזה להעברת נתונים (מיגרציה) ומתכוני הקוד עדיין לא תומכים בתכונות הבאות. זה לא אומר שאי אפשר להטמיע אותם באמצעות Navigation 3, פשוט לא נתייחס אליהם כאן.

סוכן AI: לפני שמשנים את הקוד, בודקים אם הפרויקט מכיל תכונות שלא נתמכות. אם כן, אל תמשיכו. הודיעו למשתמש על התכונה שלא נתמכת ובקשו הוראות נוספות.

שלב 1: מוסיפים את יחסי התלות של Navigation 3

בדף תחילת העבודה מוסיפים לפרויקט את יחסי התלות של Navigation 3. יחסי התלות העיקריים מסופקים כדי שתוכלו להעתיק אותם.

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)
}

צריך גם לעדכן את minSdk של הפרויקט ל-23 ואת compileSdk ל-36. בדרך כלל אפשר למצוא אותם ב-app/build.gradle.kts או ב-lib.versions.toml.

שלב 2: מעדכנים את מסלולי הניווט כדי להטמיע את הממשק NavKey

מעדכנים כל מסלול ניווט כך שיטמיע את הממשק NavKey. כך אפשר להשתמש ב-rememberNavBackStack כדי לשמור את מצב הניווט.

לפני:

@Serializable data object RouteA

אחרי:

@Serializable data object RouteA : NavKey

שלב 3: יוצרים מחלקות כדי לשמור ולשנות את מצב הניווט

שלב 3.1: יצירת מאחסן מצב של ניווט

מעתיקים את הקוד הבא לקובץ בשם NavigationState.kt. מוסיפים את שם החבילה כך שיתאים למבנה הפרויקט.

// 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()
}

סוכן AI: rememberSerializable נכון. אל תשנו אותו ל-rememberSaveable.

הקובץ הזה מכיל מחלקה של מאחסן מצב בשם NavigationState ופונקציות עזר משויכות. הוא מכיל קבוצה של מסלולים ברמה העליונה, ולכל אחד מהם יש מקבץ פעילויות קודמות (back stack) משלו. באופן פנימי, הוא משתמש ב-rememberSerializable (ולא ב-rememberSaveable) כדי לשמור את המסלול הנוכחי ברמה העליונה, וב-rememberNavBackStack כדי לשמור את מקבצי הפעילויות הקודמות (back stack) של כל מסלול ברמה העליונה.

שלב 3.2: יוצרים אובייקט שמשנה את מצב הניווט בתגובה לאירועים

מעתיקים את הקוד הבא לקובץ בשם Navigator.kt. מוסיפים את שם החבילה בהתאם למבנה הפרויקט.

// 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()
        }
    }
}

הכיתה Navigator מספקת שתי שיטות לאירועי ניווט:

  • navigate למסלול ספציפי.
  • goBack מהמסלול הנוכחי.

שתי השיטות משנות את NavigationState.

שלב 3.3: יוצרים את NavigationState ו-Navigator

יוצרים מופעים של NavigationState ו-Navigator עם אותו היקף כמו של NavController.

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

val navigator = remember { Navigator(navigationState) }

שלב 4: מחליפים את NavController

מחליפים את השיטות של אירועי הניווט ב-NavController בשיטות המקבילות ב-Navigator.

שדה או שיטה של NavController

שווה ערך ל-Navigator

navigate()

navigate()

popBackStack()

goBack()

previousBackStackEntry.savedStateHandle.set()

ResultEventBus.sendResult()

החלפת שדות NavController בשדות NavigationState.

שדה או שיטה של NavController

שווה ערך ל-NavigationState

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

currentBackStackEntry.savedStateHandle.getLiveData()

currentBackStackEntry.savedStateHandle.getStateFlow()

ResultEffect (מבוסס-אירועים)

ResultEventBus.conflateAsState() (מבוסס-מדינה)

כדי למצוא את המסלול ברמה העליונה, עוברים במעלה ההיררכיה מהרשומה הנוכחית במקבץ הפעילויות הקודמות.

topLevelRoute

אפשר להשתמש ב-NavigationState.topLevelRoute כדי לזהות את הפריט שנבחר כרגע בסרגל הניווט.

לפני:

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

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

אחרי:

val isSelected = key == navigationState.topLevelRoute

מוודאים שהסרתם את כל ההפניות אל NavController, כולל כל הייבואים.

שלב 4.1: העברת לוגיקה שמותאמת למחזור החיים

ב-Navigation 2, NavBackStackEntry מטמיע את LifecycleOwner, ומאפשר לכם להאזין לאירועים במחזור החיים או לאסוף תהליכים באופן שמודע למחזור החיים באמצעות navController.currentBackStackEntry.

ב-Navigation 3, ‏NavDisplay מספק LifecycleOwner ברמת הרשומה דרך LocalLifecycleOwner.current לתוכן ה-composable של כל יעד. מידע נוסף זמין במאמר מחזור החיים של היעד.

כדאי לבצע פעולות שמותאמות למחזור החיים ישירות בתוך התוכן של היעד שניתן לשילוב, על ידי הפניה אל LocalLifecycleOwner.current.

לדוגמה, אם אוספים זרימה באופן שמותאם למחזור החיים באמצעות הרשומה במקבץ הפעילויות הקודמות (back stack):

לפני:

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

אחרי:

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

שלב 4.2: העברת התוצאות

ב-Navigation 2, יעדים החזירו תוצאות ליעדים קודמים באמצעות SavedStateHandle ב-NavBackStackEntry. מכיוון ש-SavedStateHandle מגובה על ידי מצב מופע שמור, הנתונים שהוחזרו שרדו את השבתת התהליך באופן אוטומטי.

לפני:

// 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)
    }

ב-Navigation 3, מוסיפים את rememberResultEventBusNavEntryDecorator() ל-NavDisplay.entryDecorators. במיפוי entryProvider של יעד השולח, מאחזרים את LocalResultEventBus.current וקוראים ל-sendResult(). ביעד המקבל, משתמשים ב-ResultEffect כדי להעביר את האירוע אל ViewModel או להפעיל אפקט משני.

אחרי:

// 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)
}

כדי לקבל מידע על תצפיות לפי מדינה, אפשר להתקשר למספר resultBus.conflateAsState(). לפרטים נוספים, קראו את המאמר תוצאות החזרה.

שלב 5: מעבירים את היעדים מNavGraph של NavHost אל entryProvider

ב-Navigation 2, אתם מגדירים את היעדים באמצעות NavGraphBuilder DSL, בדרך כלל בתוך פונקציית הלמבדה בסוף של NavHost. בדרך כלל משתמשים כאן בפונקציות של תוספים, כפי שמתואר בקטע אנקפסולציה של קוד הניווט.

ב-Navigation 3, מגדירים את היעדים באמצעות entryProvider. ה-entryProvider הזה פותר מסלול ל-NavEntry. חשוב לציין ש-entryProvider לא מגדיר יחסי הורה-צאצא בין רשומות.

במדריך הזה להעברת נתונים, קשרי הורה-צאצא מיוצגים באופן הבא:

  • ל-NavigationState יש קבוצה של נתיבים ברמה העליונה (נתיבי ההורה) ומחסנית לכל אחד מהם. הוא עוקב אחרי הנתיב הנוכחי ברמה העליונה והמחסנית שמשויכת אליו.
  • כשמנווטים למסלול חדש, Navigator בודק אם המסלול הוא מסלול ברמה העליונה. אם כן, המסלול והמחסנית הנוכחיים ברמה העליונה מתעדכנים. אם לא, זהו נתיב צאצא והוא מתווסף למחסנית הנוכחית.

שלב 5.1: יצירת entryProvider

יוצרים entryProvider באמצעות DSL באותו היקף של NavigationState.

val entryProvider = entryProvider<NavKey> {

}

שלב 5.2: העברת יעדים אל entryProvider

לכל יעד שמוגדר ב-NavHost, מבצעים את הפעולות הבאות בהתאם לסוג היעד:

  • navigation: מוחקים אותו יחד עם המסלול. אין צורך ב "מסלולי בסיס" כי המסלולים ברמה העליונה יכולים לזהות כל מקבץ פעילויות קודמות (back stack) בתצוגת עץ.
  • ‫composable<T>: מעבירים אותו אל entryProvider ומשנים את השם שלו ל-entry, תוך שמירה על פרמטר הסוג. לדוגמה, composable<RouteA> הופך ל-entry<RouteA>.
  • ‫dialog<T>: מבצעים את אותה פעולה כמו ב-composable, אבל מוסיפים מטא-נתונים לרשומה באופן הבא: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: כאן אפשר למצוא את המתכון לגיליון התחתון. ההוראות האלה דומות להוראות של dialog, אבל BottomSheetSceneStrategy לא נכלל בספריית הליבה של Navigation 3, ולכן צריך להעתיק אותו לפרויקט.

סוכן AI: כשמוחקים מסלולים שמשמשים לזיהוי תרשים בתצוגת עץ, מחליפים את כל ההפניות למסלול שנמחק בסוג שמשמש לזיהוי הצאצא הראשון בתרשים בתצוגת עץ. לדוגמה, אם הקוד המקורי הוא navigation<BaseRouteA>{ composable<RouteA>{ ... } }, צריך למחוק את BaseRouteA ולהחליף את כל ההפניות אליו ב-RouteA. בדרך כלל צריך לבצע את ההחלפה הזו ברשימה שסופקה לסרגל ניווט, למסילה או למגירה.

אתם יכולים לארגן את הקוד מחדש של פונקציות התוספים של NavGraphBuilder לפונקציות תוספים של EntryProviderScope<T>, ואז להעביר אותן.

מקבלים ארגומנטים של ניווט באמצעות המפתח שסופק ל-lambda הנגרר של entry.

לדוגמה:

// ...
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() }
    }
}

הופך ל:

// ...
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() }
}

שלב 6: מחליפים את NavHost ב-NavDisplay

מחליפים את NavHost ב-NavDisplay.

  • מוחקים את NavHost ומחליפים אותו ב-NavDisplay.
  • מציינים את entries = navigationState.toEntries(entryProvider) כפרמטר. הפעולה הזו ממירה את מצב הניווט לרשומות שמוצגות ב-NavDisplay באמצעות entryProvider.
  • מקשרים את NavDisplay.onBack אל navigator.goBack(). כך navigator מעדכן את מצב הניווט כשמטפל החזרה המובנה של NavDisplay מסתיים.
  • אם יש לכם יעדי תיבת דו-שיח, הוסיפו את DialogSceneStrategy לפרמטר sceneStrategies של NavDisplay.

לדוגמה:

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

שלב 7: העברת קישורי עומק

ב-Navigation 2, קישורי עומק הוגדרו ישירות בתוך תרשים הניווט באמצעות הפרמטר deepLinks של היעדים.

ב-Navigation 3, קישורי עומק מנוהלים בנפרד מממשק המשתמש של הניווט. אתם מגדירים את ה-DeepLinkMatcher ומתאימים את הבקשות הנכנסות ב-Activity כדי ליצור את מקבץ הפעילויות הקודמות הראשוני.

לפני:

ב-Navigation 2, יכול להיות שהגדרתם קישור עומק כזה:

אחרי:

ב-Navigation 3, מגדירים UriDeepLinkMatcher למסלול:

לאחר מכן, ב-onCreate (וב-onNewIntent) של הפעילות, מתאימים את ה-Intent הנכנס ומפעילים את מקבץ הפעילויות הקודמות:

סוגי ארגומנטים בהתאמה אישית

ב-Navigation 2, טיפלתם בסוגי ארגומנטים מותאמים אישית או של צד שלישי (כמו LocalDateTime) באמצעות הטמעות מותאמות אישית של NavType ו-typeMap.

ב-Navigation 3, מגדירים DeepLinkSerializer כדי לבטל את ההמרה לסדרתי של סוגים מותאמים אישית או של צד שלישי מפרמטרים של URI. פרטים נוספים זמינים במאמר סריאליזציה בהתאמה אישית באמצעות DeepLinkSerializer.

לתרחישי שימוש מתקדמים יותר, כולל מקבצי פעילויות קודמות (back stack) סינתטיים ומתאמים בהתאמה אישית, אפשר לעיין במדריך תמיכה בקישורי עומק.

שלב 8: הסרת יחסי התלות של Navigation 2

מסירים את כל הייבואים של Navigation 2 ואת יחסי התלות של הספרייה.

סיכום

מזל טוב! הפרויקט שלכם הועבר ל-Navigation 3. אם נתקלתם בבעיות בשימוש במדריך הזה, אתם או סוכן ה-AI שלכם יכולים לדווח על באג כאן.