ניווט באמצעות 'כתיבה'

רכיב הניווט תומך ב-Jetpack כתיבת אפליקציות. אפשר לעבור בין תכנים קומפוזביליים בזמן שמנצלים את התשתית של רכיב הניווט לבינה מלאכותית גנרטיבית.

הגדרה

כדי לתמוך בכתיבה, צריך להשתמש בתלות הבאה במודול האפליקציה קובץ build.gradle:

מגניב

dependencies {
    def nav_version = "2.7.7"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.7.7"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

שנתחיל?

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

למידע על יצירת NavController במצב 'כתיבה', אפשר לעיין בקטע 'כתיבה'. בקטע יצירה של בקר ניווט.

יצירת NavHost

מידע על יצירת NavHost ב'כתיבה' מופיע בקטע 'כתיבה'. של עיצוב תרשים הניווט.

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

הניווט 'כתיבה' תומך גם בהעברת ארגומנטים בין תכנים קומפוזביליים יעדים. לשם כך, צריך להוסיף placeholders של ארגומנטים בדומה לאופן שבו מוסיפים ארגומנטים כשמשתמשים בבסיס ספריית ניווט:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

כברירת מחדל, כל הארגומנטים מנותחים כמחרוזות. הפרמטר arguments של composable() מקבל רשימה של NamedNavArgument אובייקטים. אפשר ליצור NamedNavArgument במהירות באמצעות השיטה navArgument(), מציינים את type המדויק שלו:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

צריך לחלץ את הארגומנטים מה-NavBackStackEntry זמין בעמודה lambda של הפונקציה composable().

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

כדי להעביר את הארגומנט ליעד צריך להוסיף אותו למסלול כשמבצעים את הקריאה navigate:

navController.navigate("profile/user1234")

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

אחזור נתונים מורכבים במהלך הניווט

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

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

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

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

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

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

הוספת ארגומנטים אופציונליים

התכונה 'כתיבת ניווט' תומכת גם בארגומנטים אופציונליים של ניווט. שדה אופציונלי הארגומנטים שונים מהארגומנטים הנדרשים בשתי דרכים:

  • צריך לכלול אותם באמצעות תחביר של פרמטרים של שאילתה ("?argName={argName}")
  • צריך להגדיר בהם defaultValue או להגדיר nullable = true (שמגדיר במרומז את ערך ברירת המחדל ל-null)

כלומר, צריך להוסיף באופן מפורש את כל הארגומנטים האופציונליים הפונקציה composable() משמשת כרשימה:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

עכשיו, גם אם לא מועבר ארגומנט ליעד, defaultValue, "user1234", הוא משמש במקום זאת.

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

התכונה 'כתיבה מהירה' תומכת בקישורי עומק מרומזים שניתן להגדיר את הפונקציה composable(). הפרמטר deepLinks מקבל רשימה של NavDeepLink אובייקטים שניתן ליצור במהירות באמצעות שיטת navDeepLink():

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

קישורי העומק האלה מאפשרים לך לשייך כתובת אתר, פעולה או סוג MIME ספציפיים עם קומפוזבילי. כברירת מחדל, קישורי העומק האלה לא נחשפים לאפליקציות חיצוניות. שפת תרגום כדי שקישורי העומק האלה יהיו זמינים באופן חיצוני, צריך להוסיף את רכיבי <intent-filter> לקובץ manifest.xml של האפליקציה. כדי להפעיל את האפשרות בדוגמה הקודמת, עליכם להוסיף את הטקסט הבא בתוך רכיב <activity> של המניפסט:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

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

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

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

לאחר מכן אפשר להשתמש בdeepLinkPendingIntent הזה כמו בכל PendingIntent אחר כדי פותחים את האפליקציה ביעד של קישור העומק.

ניווט בתוך רכיב

למידע על יצירת תרשימי ניווט מקוננים, ראה תרשימים מקוננים.

שילוב עם סרגל הניווט התחתון

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

כדי להשתמש ברכיבים BottomNavigation ו-BottomNavigationItem, להוסיף את התלות androidx.compose.material לאפליקציית Android שלך.

מגניב

dependencies {
    implementation "androidx.compose.material:material:1.6.8"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.6.8")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

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

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

לאחר מכן הציבו את הפריטים האלה ברשימה שבה אפשר להשתמש BottomNavigationItem:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

בתוכן הקומפוזבילי BottomNavigation, מקבלים את הערך הנוכחי של NavBackStackEntry באמצעות הפונקציה currentBackStackEntryAsState(). רשומה זו מעניקה לך גישה אל NavDestination הנוכחי. המצב שנבחר של כל רכיב לאחר מכן ניתן יהיה לקבוע את BottomNavigationItem על ידי השוואה בין המסלול של הפריט במסלול של היעד הנוכחי וביעדי ההורה שלו כאשר משתמשים בניווט בתצוגת עץ, באמצעות הפונקציה היררכיה של NavDestination.

מסלול הפריט משמש גם לחיבור ה-lambda onClick לקריאה אל navigate, כך שהקשה על הפריט תנווט לאותו פריט. על ידי שימוש הדגלים saveState ו-restoreState, את המצב (State) והמקבץ האחורי הפריט נשמר ומשוחזר כמו שצריך כשעוברים בין הניווט התחתון פריטים.

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.route) {
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
              }
              // Avoid multiple copies of the same destination when
              // reselecting the same item
              launchSingleTop = true
              // Restore state when reselecting a previously selected item
              restoreState = true
            }
          }
        )
      }
    }
  }
) { innerPadding ->
  NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { FriendsList(navController) }
  }
}

כאן אפשר לנצל את NavController.currentBackStackEntryAsState() כדי להוציא את המצב navController מהפונקציה NavHost, לשתף אותו עם הרכיב BottomNavigation. המשמעות היא המצב העדכני ביותר של BottomNavigation הוא באופן אוטומטי.

בטיחות סוג הכתיבה בניווט

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

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

למידע נוסף בנושא זה, ראו בטיחות סוג ב-Kotlin DSL וניווט פיתוח נייטיב.

יכולת פעולה הדדית

אם ברצונך להשתמש ברכיב הניווט במצב 'כתיבה', יש לך שתי אפשרויות:

  • הגדרת תרשים ניווט עם רכיב הניווט של מקטעים.
  • הגדרת תרשים ניווט עם NavHost בקטע 'כתיבה' באמצעות 'כתיבה' יעדים. הדבר אפשרי רק אם כל המסכים בניווט הם תכנים קומפוזביליים.

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

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

@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

בקטע שלך, אתה יוצר את הגשר בין 'פיתוח נייטיב' לבין המקטע רכיב הניווט על ידי איתור ה-NavController ומעבר אל destination:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

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

בדיקה

צריך להסתיר את קוד הניווט מהיעדים הקומפוזביליים כדי להפעיל את הבדיקה כל תוכן קומפוזבילי לבודד, בנפרד מהתוכן הקומפוזבילי NavHost.

כלומר, אין להעביר את navController ישירות אל composable ובמקום זאת להעביר קריאות חוזרות (callback) של ניווט כפרמטרים. כך אפשר את כל התכנים הקומפוזביליים שלכם כך שיהיו ניתנים לבדיקה בנפרד, כי הם לא דורשים מופע של navController בבדיקות.

רמת העקיפה שמסופקת על ידי lambda של composable מאפשרת לך להפריד את קוד הניווט מהתוכן הקומפוזבילי עצמו. זה עובד בשני הוראות הגעה:

  • העברת רק ארגומנטים מנותחים לתוכן הקומפוזבילי
  • מעבירים lambdas שאמורות להיות מופעלות על ידי התוכן הקומפוזבילי לניווט, ולא ב-NavController עצמו.

לדוגמה, תוכן קומפוזבילי Profile שמקבל כקלט userId ומאפשר שמשתמשים שינווטו לדף פרופיל של חבר, עשויים להיות בעלי החתימה של:

@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 
}

כך, התוכן הקומפוזבילי Profile פועל בנפרד מהניווט, ומאפשרת לבדוק אותו באופן עצמאי. ה-lambda של composable היא כוללת את הלוגיקה המינימלית הנדרשת לגשר על הפער בין הניווט ממשקי API והתוכן הקומפוזבילי:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

מומלץ לכתוב בדיקות שעונות על דרישות הניווט באפליקציה על ידי בדיקה של NavHost, פעולות הניווט עברו בתכנים הקומפוזביליים וגם בתכנים הקומפוזביליים האישיים שלכם.

בדיקה של NavHost

כדי להתחיל לבדוק את NavHost , צריך להוסיף את בדיקות הניווט הבאות של תלות:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

אפשר להגדיר את נושא הבחינה NavHost ולעבור של המופע navController. כדי לעשות את זה, הניווט פריט המידע שנוצר בתהליך הפיתוח (Artifact) מספק TestNavHostController. בדיקה של ממשק משתמש מאמת את יעד ההתחלה של האפליקציה, והחלק NavHost ייראה כך:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    // Unit test
    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

בדיקה של פעולות הניווט

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

כדי לבדוק את ההטמעה בפועל של האפליקציה, צריך ללחוץ על עדיף להשתמש בממשק משתמש. כדי ללמוד איך לבדוק את זה לצד תכנים קומפוזביליים נפרדים לבידוד, הקפידו לבדוק בדיקות ב-codelab ב-Jetpack פיתוח נייטיב.

אפשר גם להשתמש ב-navController כדי לבדוק את טענות הנכוֹנוּת באמצעות השוואה בין נתיב המחרוזת הנוכחי למסלול הצפוי, באמצעות currentBackStackEntry של navController:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

לקבלת הנחיות נוספות לגבי העקרונות הבסיסיים של הבדיקה בכתיבה, אפשר לעיין במאמר בדיקת פריסת הכתיבה ובדיקות ב-Jetpack פיתוח נייטיב Codelab. למידע נוסף על בדיקה מתקדמת של קוד הניווט, המדריך בדיקת ניווט.

מידע נוסף

מידע נוסף על הניווט ב-Jetpack זמין במאמר תחילת העבודה עם הניווט רכיב או לקחת את Jetpack כתיבת קוד Lab לניווט.

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

כדי לקבל מידע נוסף על הטמעת ניווט מתקדמת יותר ב'כתיבה' אפליקציה מודולרית, כולל מושגים כמו תרשימים בתוך תרשימים וסרגל ניווט תחתון בשילוב עם האפליקציה Now ב-Android ב-GitHub.

דוגמיות