רכיב הניווט מספק תמיכה באפליקציות של Jetpack פיתוח נייטיב. אתם יכולים לנווט בין רכיבים מורכבים תוך ניצול התשתית והתכונות של רכיב הניווט.
הגדרה
כדי לתמוך ב-Compose, צריך להוסיף את התלות הבאה לקובץ build.gradle
של מודול האפליקציה:
Groovy
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
הנוכחי. כדי לטפל במקרים שבהם משתמשים בניווט בתצוגת עץ באמצעות היררכיית NavDestination
, אפשר לקבוע את המצב שנבחר לכל BottomNavigationItem
על ידי השוואה בין הנתיב של הפריט לבין הנתיב של היעד הנוכחי ויעדי ההורה שלו.
המסלול של הפריט משמש גם כדי לחבר את ה-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 ו-Views היא להשתמש ברכיב הניווט שמבוסס על קטעים. לאחר מכן, קטעי הקוד יכללו מסכים שמבוססים על תצוגות, מסכים של 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
ישירות לרכיב מורכב כלשהו, אלא להעביר קריאות חזרה (callbacks) של ניווט כפרמטרים. כך תוכלו לבדוק כל רכיב בנפרד, כי לא נדרש מופע של navController
בבדיקות.
רמת העקיף שמספקת הפונקציה הלמבדת 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()
}
}
בדיקת פעולות ניווט
יש כמה דרכים לבדוק את הטמעת הניווט: אפשר ללחוץ על רכיבי ממשק המשתמש ולאמת את היעד שמוצג, או להשוות בין המסלול הצפוי למסלול הנוכחי.
מכיוון שאתם רוצים לבדוק את ההטמעה של האפליקציה הספציפית שלכם, עדיף לבצע קליקים בממשק המשתמש. כדי ללמוד איך לבדוק את זה לצד פונקציות מורכבות נפרדות, מומלץ לעיין בקודלאב בדיקות ב-Jetpack Compose.
אפשר גם להשתמש ב-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
- איפה להעלות את המצב