רכיב הניווט מספק תמיכה באפליקציות Jetpack פיתוח נייטיב. אתם יכולים לנווט בין קומפוזיציות תוך ניצול התשתית והתכונות של רכיב הניווט.
למידע על ספריית הניווט העדכנית בגרסת אלפא שנוצרה במיוחד עבור Compose, אפשר לעיין במסמכי התיעוד של Navigation 3.
הגדרה
כדי לתמוך ב-Compose, משתמשים בתלות הבאה בקובץ build.gradle
של מודול האפליקציה:
Groovy
dependencies { def nav_version = "2.9.1" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.1" implementation("androidx.navigation:navigation-compose:$nav_version") }
שנתחיל?
כשמטמיעים ניווט באפליקציה, צריך להטמיע מארח ניווט, תרשים ובקר. מידע נוסף זמין במאמר בנושא ניווט.
יצירת NavController
בקטע Compose במאמר יצירת בקר ניווט מוסבר איך ליצור NavController
ב-Compose.
יצירת NavHost
מידע על יצירת NavHost
ב-Compose זמין בקטע Compose במאמר עיצוב גרף הניווט.
ניווט אל קומפוזבל
במאמר ניווט ליעד במסמכי הארכיטקטורה מוסבר איך מגיעים לרכיב שאפשר להרכיב.
ניווט באמצעות ארגומנטים
מידע על העברת ארגומנטים בין יעדים שאפשר להרכיב מופיע בקטע 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)
// …
}
הגישה הזו עוזרת למנוע אובדן נתונים במהלך שינויים בהגדרות, וגם חוסר עקביות כשמעדכנים או משנים את האובייקט הרלוונטי.
הסבר מפורט יותר על הסיבות לכך שכדאי להימנע מהעברת נתונים מורכבים כארגומנטים, ורשימה של סוגי הארגומנטים הנתמכים, מופיעים במאמר העברת נתונים בין יעדים.
קישורי עומק
Navigation Compose תומך גם בקישורי עומק שאפשר להגדיר כחלק מהפונקציה composable()
. הפרמטר deepLinks
שלו מקבל רשימה של אובייקטים מסוג NavDeepLink
שאפשר ליצור במהירות באמצעות ה-method 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.8.3" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.8.3") } 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
.
הנתיב של הפריט משמש גם לחיבור של פונקציית ה-onClick
lambda לקריאה אל 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
מתעדכן באופן אוטומטי.
יכולת פעולה הדדית
אם רוצים להשתמש ברכיב Navigation עם Compose, יש שתי אפשרויות:
- הגדרת תרשים ניווט באמצעות רכיב הניווט עבור פרגמנטים.
- מגדירים גרף ניווט עם
NavHost
ב-Compose באמצעות יעדי Compose. זה אפשרי רק אם כל המסכים בתרשים הניווט הם קומפוזיציות.
לכן, ההמלצה לאפליקציות שמשלבות בין Compose ו-Views היא להשתמש ברכיב Fragment-based Navigation. לאחר מכן, הרכיבים יכילו מסכים מבוססי-View, מסכי Compose ומסכים שמשתמשים גם ב-Views וגם ב-Compose. אחרי שכל התוכן של Fragment נמצא ב-Compose, השלב הבא הוא לקשר בין כל המסכים באמצעות Navigation Compose ולהסיר את כל ה-Fragments.
ניווט מ-Compose באמצעות Navigation for fragments
כדי לשנות יעדים בתוך קוד 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
בבדיקות.
רמת ההפניה העקיפה שמספקת פונקציית ה-composable
lambda מאפשרת להפריד את קוד הניווט מהקומפוזיציה עצמה. הפעולה הזו מתבצעת בשני כיוונים:
- העברה רק של ארגומנטים מנותחים לרכיב הניתן להרכבה
- צריך להעביר פונקציות למדא שמופעלות על ידי הפונקציה הניתנת להרכבה כדי לנווט, במקום להעביר את
NavController
עצמו.
לדוגמה, ProfileScreen
קומפוזבל שמקבל userId
כקלט ומאפשר למשתמשים לנווט לדף הפרופיל של חבר יכול להיות עם החתימה הבאה:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
כך, רכיב ה-ProfileScreen
composable פועל בנפרד מהרכיב Navigation, ולכן אפשר לבדוק אותו בנפרד. פונקציית ה-lambda composable
תכיל את הלוגיקה המינימלית שנדרשת כדי לגשר על הפער בין ממשקי ה-API של Navigation לבין הרכיב שאפשר להרכיב:
@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
של האפליקציה ברכיב שאפשר להרכיב ממנו רכיבים אחרים, שמקבל 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 Compose.
אפשר גם להשתמש ב-navController
כדי לבדוק את הטענות על ידי השוואת המסלול הנוכחי למסלול הצפוי, באמצעות navController
של currentBackStackEntry
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
לקבלת הנחיות נוספות בנושא יסודות הבדיקה ב-Compose, אפשר לעיין במאמר בדיקת פריסת Compose וב-codelab Testing in Jetpack Compose. מידע נוסף על בדיקה מתקדמת של קוד הניווט זמין במדריך בנושא בדיקת ניווט.
מידע נוסף
מידע נוסף על Jetpack Navigation זמין במאמר תחילת העבודה עם רכיב הניווט או ב-Jetpack Compose Navigation codelab.
כדי ללמוד איך לעצב את הניווט באפליקציה כך שיתאים לגדלים, לכיוונים ולגורמי צורה שונים של מסכים, אפשר לעיין במאמר ניווט בממשקי משתמש רספונסיביים.
כדי לקבל מידע על הטמעה מתקדמת יותר של ניווט ב-Compose באפליקציה מודולרית, כולל מושגים כמו גרפים מוטמעים ושילוב של סרגל ניווט בתחתית, אפשר לעיין באפליקציה Now in Android ב-GitHub.
טעימות
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- Material Design 2 ב-Compose
- העברת Jetpack Navigation ל-Navigation Compose
- איפה כדאי להעביר את הסטייט