ניווט באמצעות 'כתיבה'

רכיב הניווט מספק תמיכה באפליקציות Jetpack פיתוח נייטיב. אתם יכולים לנווט בין קומפוזיציות תוך ניצול התשתית והתכונות של רכיב הניווט.

לספריית הניווט העדכנית ביותר בגרסת אלפא, שנוצרה במיוחד עבור Compose, אפשר לעיין במסמכי התיעוד של Navigation 3.

הגדרה

כדי לתמוך ב-Compose, צריך להשתמש בתלות הבאה בקובץ build.gradle של מודול האפליקציה:

Groovy

dependencies {
    def nav_version = "2.9.4"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.9.4"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

שנתחיל?

כשמטמיעים ניווט באפליקציה, צריך להטמיע מארח ניווט, גרף ובקר. מידע נוסף זמין במאמר בנושא ניווט.

בקטע 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 שבו האפליקציה מוצגת. במסכים קומפקטיים, NavigationSuiteScaffold מציג סרגל ניווט תחתון. במסך מורחב, מוצג במקום זאת סרגל ניווט.

מידע נוסף זמין במאמר יצירת ניווט מותאם.

יכולת פעולה הדדית

אם רוצים להשתמש ברכיב Navigation עם Compose, יש שתי אפשרויות:

  • הגדרת תרשים ניווט באמצעות רכיב הניווט עבור פרגמנטים.
  • מגדירים גרף ניווט עם NavHost ב-Compose באמצעות יעדי Compose. זה אפשרי רק אם כל המסכים בתרשים הניווט הם קומפוזיציות.

לכן, ההמלצה לאפליקציות עם תמהיל של Compose ו-Views היא להשתמש ברכיב Fragment-based Navigation. לאחר מכן, הפעולות יתבצעו על מסכים מבוססי-View, מסכי Compose ומסכים שמשתמשים גם ב-Views וגם ב-Compose. אחרי שמעבירים את התוכן של כל Fragment ל-Compose, השלב הבא הוא לקשר בין כל המסכים באמצעות Navigation Compose ולהסיר את כל ה-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 ישירות לכלרכיב שאפשר להרכיב, אלא צריך להעביר פונקציות קריאה חוזרת של ניווט כפרמטרים. כך אפשר לבדוק כל קומפוזיציה בנפרד, כי לא צריך מופע של navController בבדיקות.

רמת ההפניה העקיפה שמספקת פונקציית ה-lambda‏ composable מאפשרת להפריד את קוד הניווט מהקומפוזיציה עצמה. הפעולה הזו מתבצעת בשני כיוונים:

  • העברה רק של ארגומנטים מנותחים לרכיב ה-Composable
  • צריך להעביר פונקציות למדא שמופעלות על ידי הפונקציה הניתנת להרכבה כדי לנווט, ולא את 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 בנושא בדיקה ב-Jetpack Compose. מידע נוסף על בדיקה מתקדמת של קוד הניווט זמין במדריך בנושא בדיקת ניווט.

מידע נוסף

מידע נוסף על Jetpack Navigation זמין במאמר תחילת העבודה עם רכיב הניווט או ב-Jetpack Compose Navigation codelab.

כדי ללמוד איך לעצב את הניווט באפליקציה כך שיתאים לגדלים, לכיוונים ולגורמי צורה שונים של מסכים, אפשר לעיין במאמר ניווט בממשקי משתמש רספונסיביים.

כדי ללמוד על הטמעה מתקדמת יותר של ניווט ב-Compose באפליקציה מודולרית, כולל מושגים כמו גרפים מוטמעים ושילוב של סרגל ניווט בתחתית, אפשר לעיין באפליקציה Now in Android ב-GitHub.

טעימות