רכיב הניווט מספק תמיכה באפליקציות של Jetpack פיתוח נייטיב. אתם יכולים לנווט בין רכיבים מורכבים תוך ניצול התשתית והתכונות של רכיב הניווט.
הגדרה
כדי לתמוך ב-Compose, צריך להוסיף את התלות הבאה לקובץ build.gradle
של מודול האפליקציה:
מגניב
dependencies { def nav_version = "2.8.0" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.0" implementation("androidx.navigation:navigation-compose:$nav_version") }
שנתחיל?
כשמטמיעים ניווט באפליקציה, צריך להטמיע מארח ניווט, תרשים ובקר. מידע נוסף זמין בקטע 'סקירה כללית על ניווט'.
יצירת NavController
מידע על יצירת NavController
ב-Compose זמין בקטע Compose במאמר יצירת בקר ניווט.
יצירת NavHost
מידע על יצירת NavHost
ב-Compose זמין בקטע Compose במאמר עיצוב של תרשים הניווט.
ניווט לרכיב שאפשר לשלב
למידע על ניווט ל-Composable, ראו ניווט ליעד במסמכי העזרה בנושא ארכיטקטורה.
ניווט באמצעות ארגומנטים
מידע על העברת ארגומנטים בין יעדים שאפשר ליצור מהם קומפוזיציות זמין בקטע Compose במאמר תכנון של תרשים הניווט.
אחזור נתונים מורכבים במהלך הניווט
מומלץ מאוד לא להעביר אובייקטים מורכבים של נתונים במהלך הניווט, אלא להעביר את המידע הנדרש המינימלי, כמו מזהה ייחודי או סוג אחר של מזהה, כארגומנטים בזמן ביצוע פעולות ניווט:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
אובייקטים מורכבים צריכים להיות מאוחסנים כנתונים במקור מהימן אחד, כמו שכבת הנתונים. אחרי שמגיעים ליעד אחרי הניווט, אפשר לטעון את המידע הנדרש ממקור מהימן אחד באמצעות המזהה שהועבר. כדי לאחזר את הארגומנטים ב-ViewModel
שאחראי לגישה לשכבת הנתונים, משתמשים ב-SavedStateHandle
של ה-ViewModel
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
הגישה הזו עוזרת למנוע אובדן נתונים במהלך שינויים בתצורה, וגם חוסר עקביות כשמתבצע עדכון או שינוי של האובייקט הרלוונטי.
הסבר מפורט יותר על הסיבות להימנע מהעברת נתונים מורכבים כארגומנטים, וכן רשימה של סוגי הארגומנטים הנתמכים, זמין במאמר העברת נתונים בין יעדים.
קישורי עומק
ההצעות לכתיבה תומכות בקישורי עומק שאפשר להגדיר גם כחלק מהפונקציה composable()
. הפרמטר deepLinks
מקבל רשימה של אובייקטים מסוג NavDeepLink
, שאפשר ליצור במהירות באמצעות השיטה navDeepLink()
:
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}
קישורי העומק האלה מאפשרים לכם לשייך כתובת URL, פעולה או סוג 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/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
לאחר מכן תוכלו להשתמש ב-deepLinkPendingIntent
הזה כמו בכל PendingIntent
אחר כדי לפתוח את האפליקציה ביעד של קישור העומק.
ניווט בתצוגת עץ
מידע נוסף על יצירת תרשימי ניווט בתצוגת עץ זמין במאמר תרשימים בתצוגת עץ.
שילוב עם סרגל הניווט התחתון
הגדרת NavController
ברמה גבוהה יותר בהיררכיה הניתנת ליצירה מאפשרת לקשר את Navigation לרכיבים אחרים, כמו רכיב הניווט התחתון. כך תוכלו לנווט על ידי בחירת הסמלים בסרגל התחתון.
כדי להשתמש ברכיבים BottomNavigation
ו-BottomNavigationItem
, צריך להוסיף את התלות androidx.compose.material
לאפליקציה ל-Android.
Groovy
dependencies { implementation "androidx.compose.material:material:1.7.5" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.5") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
כדי לקשר את הפריטים בסרגל הניווט התחתון למסלולים בתרשים הניווט, מומלץ להגדיר סוג, כמו TopLevelRoute
שמוצג כאן, שיש לו סוג מסלול וסמל.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
לאחר מכן, מוסיפים את המסלולים האלה לרשימה שאפשר להשתמש בה ב-BottomNavigationItem
:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
ב-composable של BottomNavigation
, מקבלים את הערך הנוכחי של NavBackStackEntry
באמצעות הפונקציה currentBackStackEntryAsState()
. הרשומה הזו נותנת לכם גישה ל-NavDestination
הנוכחי. לאחר מכן, אפשר לקבוע את המצב של כל מאפיין BottomNavigationItem
על ידי השוואה בין המסלול של הפריט למסלול של היעד הנוכחי והיעדים ברמת ההורה, כדי לטפל במקרים שבהם משתמשים בניווט בתוך רכיב בעזרת ההיררכיה של NavDestination
.
המסלול של הפריט משמש גם כדי לחבר את ה-lambda של onClick
לקריאה ל-navigate
, כך שהקשה על הפריט תוביל אליו. באמצעות הדגלים saveState
ו-restoreState
, המצב והערימה האחורית של אותו פריט נשמרים ומשוחזרים כשעוברים בין פריטי הניווט התחתונים.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.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 = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
כאן נעשה שימוש בשיטה NavController.currentBackStackEntryAsState()
כדי להעביר את המצב navController
מהפונקציה NavHost
ולשתף אותו עם הרכיב BottomNavigation
. כלומר,
ה-BottomNavigation
יהיה במצב העדכני ביותר באופן אוטומטי.
יכולת פעולה הדדית
אם אתם רוצים להשתמש ברכיב הניווט עם Compose, יש לכם שתי אפשרויות:
- הגדרת תרשים ניווט עם רכיב הניווט של מקטעים.
- מגדירים גרף ניווט עם
NavHost
ב-Compose באמצעות יעדים של Compose. אפשר לעשות זאת רק אם כל המסכים בתרשים הניווט הם רכיבים שאפשר ליצור מהם קומפוזיציות.
לכן, ההמלצה לאפליקציות משולבות 'כתיבה' ו'תצוגות' היא להשתמש ברכיב הניווט מבוסס מקטעים. לאחר מכן, קטעי הקוד יכללו מסכים שמבוססים על תצוגות, מסכים של Compose ומסכים שמשתמשים גם בתצוגות וגם ב-Compose. אחרי שהתוכן של כל ה-Fragments נמצא ב-Compose, השלב הבא הוא לקשר את כל המסכים האלה יחד עם Navigation Compose ולהסיר את כל ה-Fragments.
ניווט מ-Compose באמצעות ניווט בקטעים
כדי לשנות את היעדים בקוד של Compose, צריך לחשוף אירועים שאפשר להעביר לכל רכיב מורכב בהיררכיה, ושהרכיב יכול להפעיל:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
כדי ליצור את הגשר בין Compose לבין רכיב הניווט שמבוסס על הפלח, צריך למצוא את NavController
ולנווט ליעד:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
לחלופין, אפשר להעביר את NavController
במורד היררכיית ה-Compose.
עם זאת, חשיפה של פונקציות פשוטות מאפשרת שימוש חוזר ובדיקה בקלות רבה יותר.
בדיקה
מנתקים את קוד הניווט מהיעדים הניתנים לקישור כדי לאפשר בדיקה של כל רכיב בנפרד, בנפרד מהרכיב NavHost
.
כלומר, לא מומלץ להעביר את navController
ישירות לכל תוכן קומפוזבילי, אלא להעביר קריאות חוזרות (callback) של ניווט בתור פרמטרים. כך תוכלו לבדוק כל רכיב בנפרד, כי לא נדרש מופע של navController
בבדיקות.
רמת העקיפה שמסופקת על ידי lambda של composable
היא שמאפשרת להפריד את קוד הניווט מהתוכן הקומפוזבילי עצמו. ניתן לעשות זאת בשתי דרכים:
- מעבירים רק ארגומנטים שעברו ניתוח ל-composable
- מעבירים פונקציות lambda שצריכות להיות מופעלות על ידי ה-composable כדי לנווט, במקום את
NavController
עצמו.
לדוגמה, רכיב ProfileScreen
שמורכב מ-userId
כקלט ומאפשר למשתמשים לנווט לדף הפרופיל של חבר יכול להיות עם החתימה הבאה:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
כך, הרכיב הניתן לקיבוץ של ProfileScreen
פועל בנפרד מ-Navigation, ומאפשר לבדוק אותו בנפרד. פונקציית הלמה של composable
תכיל את הלוגיקה המינימלית הנדרשת כדי לגשר על הפער בין ממשקי ה-Navigation API לבין הרכיב הניתן לקיבוץ:
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
מומלץ לכתוב בדיקות שמכסות את דרישות הניווט באפליקציה. לשם כך, צריך לבדוק את NavHost
, את פעולות הניווט שמועברות לרכיבי ה-Composable ואת רכיבי ה-Composable של המסך הספציפי.
בדיקת NavHost
כדי להתחיל לבדוק את NavHost
, מוסיפים את התלות הבאה לבדיקת הניווט:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
עוטפים את NavHost
של האפליקציה ב-composable שמקבל NavHostController
כפרמטר.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
עכשיו אפשר לבדוק את AppNavHost
ואת כל לוגיקת הניווט שמוגדרת ב-NavHost
על ידי העברת מופע של הארטיפקט לבדיקה של הניווט 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
כדי לבדוק את ההצהרות (assertions) על ידי השוואה בין המסלול הנוכחי למסלול הצפוי, באמצעות currentBackStackEntry
של navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
למידע נוסף על היסודות של בדיקת Compose, תוכלו לעיין במאמר בדיקת הפריסה של Compose ובקורס ה-codelab בדיקת Jetpack Compose. מידע נוסף על בדיקה מתקדמת של קוד הניווט זמין במדריך בדיקת הניווט.
מידע נוסף
למידע נוסף על ניווט ב-Jetpack, אפשר לעיין במאמר תחילת העבודה עם רכיב הניווט או להשתתף בcodelab בנושא ניווט ב-Jetpack פיתוח נייטיב.
במאמר ניווט בממשקי משתמש רספונסיביים מוסבר איך לתכנן את הניווט באפליקציה כך שיתאים לגדלים, לכיוונים ולפורמטים שונים של מסכים.
כדי לקבל מידע על הטמעה מתקדמת יותר של הניווט ב-Compose באפליקציה מודולרית, כולל מושגים כמו תרשימים בתצוגת עץ ושילוב של סרגל הניווט התחתון, אפשר לעיין באפליקציה Now in Android ב-GitHub.
דוגמיות
מומלץ עבורך
- הערה: טקסט הקישור מוצג כאשר JavaScript מושבת
- עיצוב Material 2 ב-Compose
- העברת Jetpack Navigation ל-Navigation Compose
- איפה להעלות את המצב