רכיב הניווט תומך ב-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
למידע על יצירת 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 שלך.
Groovy
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.
דוגמיות
מומלץ עבורך
- הערה: טקסט הקישור מוצג כאשר JavaScript מושבת
- עיצוב Material 2 ב-Compose
- העברת הניווט ב-Jetpack לכתיבה קולית
- איפה להעלות את המצב