רכיב הניווט מספק תמיכה באפליקציות Jetpack פיתוח נייטיב. אתם יכולים לנווט בין רכיבי Composable תוך ניצול התשתית והתכונות של רכיב הניווט.
במסמכי התיעוד של Navigation 3 אפשר לקרוא על ספריית הניווט העדכנית של גרסת טרום-ההפצה, שנבנתה במיוחד בשביל Compose.
הגדרה
כדי לתמוך ב-Compose, משתמשים בתלות הבאה בקובץ build.gradle של מודול האפליקציה:
Groovy
dependencies { def nav_version = "2.9.8" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.8" implementation("androidx.navigation:navigation-compose:$nav_version") }
שנתחיל?
כשמטמיעים ניווט באפליקציה, צריך להטמיע מארח ניווט, גרף ובקר. מידע נוסף זמין במאמר סקירה כללית על ניווט.
יצירת NavController
בקטע Compose במאמר יצירת בקר ניווט מוסבר איך ליצור NavController ב-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)
// …
}
הגישה הזו עוזרת למנוע אובדן נתונים במהלך שינויים בהגדרות, וגם חוסר עקביות כשמעדכנים או משנים את האובייקט הרלוונטי.
הסבר מפורט יותר על הסיבות לכך שכדאי להימנע מהעברת נתונים מורכבים כארגומנטים, ורשימה של סוגי הארגומנטים הנתמכים, מופיעים במאמר העברת נתונים בין יעדים.
קישורי עומק
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 אחר כדי לפתוח את האפליקציה ביעד של קישור העומק.
ניווט מקונן
מידע על יצירת תרשימי ניווט בתצוגת עץ זמין במאמר בנושא תרשימים בתצוגת עץ.
איך בונים סרגל ניווט ופס ניווט דינמיים
ב-NavigationSuiteScaffold מוצג ממשק המשתמש המתאים לניווט ברמה העליונה של האפליקציה, על סמך WindowSizeClass הנוכחי. במסכים קומפקטיים, ה-scaffold מציג סרגל ניווט תחתון. במסכים בינוניים ובמסכים מורחבים, הוא מציג פס ניווט.
NavigationSuiteScaffold מטפל בניווט הראשי, אבל פריסות דינמיות לרוב כוללות רכיבים קומפוזביליים מיוחדים אחרים. לפריסות קנוניות של חלונית תומכת ושל רשימה עם פרטים, שמשמשות בדרך כלל בעיצובים דינמיים, משתמשים ב-ListDetailPaneScaffold וב-SupportingPaneScaffold, בהתאמה.
מידע נוסף זמין במאמר בנושא יצירת פריסות דינמיות.
יכולת פעולה הדדית
אם אתם רוצים להשתמש ברכיב Navigation עם Compose, יש לכם שתי אפשרויות:
- הגדרת תרשים ניווט באמצעות רכיב הניווט בשברים.
- מגדירים גרף ניווט עם
NavHostב-Compose באמצעות יעדי Compose. זה אפשרי רק אם כל המסכים בתרשים הניווט הם קומפוזיציות.
לכן, ההמלצה לאפליקציות מעורבות של פיתוח נייטיב ו-Views היא להשתמש ברכיב הניווט מבוסס-מקטע (fragment). המקטעים יכילו מסכים מבוססי-View, מסכי פיתוח נייטיב ומסכים שמשתמשים גם ב-View וגם בפיתוח נייטיב. אחרי שהתוכן של כל מקטע (fragment) נמצא ב-Compose, השלב הבא הוא לקשר בין כל המסכים באמצעות Navigation Compose ולהסיר את כל המקטעים.
ניווט מ-Compose באמצעות Navigation for fragments
כדי לשנות יעדים בתוך קוד Compose, צריך לחשוף אירועים שאפשר להעביר ולהפעיל על ידי כל רכיב שאפשר להרכיב בהיררכיה:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
במקטע (fragment), יוצרים את הגשר בין Compose לבין רכיב הניווט מבוסס-מקטע (fragment) על ידי חיפוש NavController וניווט ליעד:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
אפשר גם להעביר את NavController בהיררכיית ה-Compose.
עם זאת, חשיפת פונקציות מאפשרת שימוש חוזר וקל יותר בבדיקה.
בדיקה
כדי לבדוק כל קומפוזיציה בנפרד, בלי קשר לקומפוזיציה NavHost, צריך להפריד את קוד הניווט מהיעדים הניתנים לקומפוזיציה.
כלומר, לא צריך להעביר את navController ישירות לכלרכיב שאפשר להרכיב, אלא להעביר פונקציות קריאה חוזרת של ניווט כפרמטרים. כך אפשר לבדוק כל קומפוזיציה בנפרד, כי לא צריך מופע של navController בבדיקות.
רמת ההפניה העקיפה שמספקת פונקציית ה-lambda composable מאפשרת להפריד את קוד הניווט מהקומפוזיציה עצמה. הפעולה הזו מתבצעת בשני כיוונים:
- העברת ארגומנטים מנותחים בלבד לרכיב ה-Composable
- מעבירים ביטויי למדה (lambda) שצריכים להיות מופעלים על ידי הפונקציה הניתנת להגדרה כדי לנווט, ולא את
NavControllerעצמה.
לדוגמה, ProfileScreen קומפוזבל שמקבל userId כקלט ומאפשר למשתמשים לנווט לדף הפרופיל של חבר יכול להיות בעל החתימה הבאה:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
כך, רכיב ה-ProfileScreen composable פועל בנפרד מ-Navigation, ולכן אפשר לבדוק אותו בנפרד. פונקציית ה-lambda composable תכיל את הלוגיקה המינימלית שנדרשת כדי לגשר על הפער בין ממשקי ה-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 של האפליקציה בקומפוזיציה שמקבלת 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 כדי לבדוק את טענות הנכוֹנוּת על ידי השוואת המסלול הנוכחי למסלול הצפוי, באמצעות navController של currentBackStackEntry:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
במאמר בדיקת פריסת פיתוח נייטיב וב-codelab בדיקות ב-Jetpack פיתוח נייטיב אפשר לקרוא מידע נוסף על היסודות של בדיקות ב-Compose. מידע נוסף על בדיקה מתקדמת של קוד הניווט זמין במדריך בנושא בדיקת ניווט.
מידע נוסף
מידע נוסף על Jetpack Navigation זמין במאמר תחילת העבודה עם רכיב הניווט או ב-Codelab Jetpack פיתוח נייטיב Navigation.
כדי ללמוד איך לעצב את הניווט באפליקציה כך שיתאים לגדלים, לכיוונים ולגורמי צורה שונים של מסכים, אפשר לעיין במאמר ניווט בממשקי משתמש רספונסיביים.
כדי ללמוד על הטמעה מתקדמת יותר של ניווט ב-Compose באפליקציה מודולרית, כולל מושגים כמו גרפים מוטמעים ושילוב של סרגל ניווט בתחתית, אפשר לעיין באפליקציה Now in Android ב-GitHub.
דוגמאות
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- Material Design 2 ב-Compose
- העברת נתונים מ-Jetpack Navigation ל-Navigation Compose
- איפה כדאי להעלות את הסטייט