Nawigacja przy tworzeniu wiadomości

Komponent Nawigacja obsługuje aplikacje Jetpack Compose. Możesz poruszać się między komponentami, korzystając z ich infrastruktury i funkcji.

Konfiguracja

Aby obsługiwać Compose, użyj w pliku build.gradle modułu aplikacji tej zależności:

Groovy

dependencies {
    def nav_version = "2.8.5"

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

Kotlin

dependencies {
    val nav_version = "2.8.5"

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

Rozpocznij

Podczas implementowania nawigacji w aplikacji zastosuj hosta, wykres i sterownik nawigacji. Więcej informacji znajdziesz w artykule Nawigacja.

Informacje o tworzeniu elementu NavController w sekcji „Compose” (Utwórz) znajdziesz w sekcji Tworzenie kontrolera nawigacji.

Tworzenie elementu NavHost

Informacje o tworzeniu NavHost w sekcji Kompozycja znajdziesz w artykule Projektowanie grafu nawigacji.

Informacje o nawigowaniu do komponentu znajdziesz w sekcji Nawigowanie do miejsca docelowego w dokumentacji architektury.

Informacje o przekazywaniu argumentów między miejscami docelowymi do składania znajdziesz w sekcji „Tworzenie” w artykule Projektowanie wykresu nawigacji.

Pobieranie złożonych danych podczas nawigacji

Zdecydowanie zalecamy, aby podczas nawigacji nie przekazywać skomplikowanych obiektów danych, a zamiast tego przekazywać jako argumenty podczas wykonywania działań nawigacyjnych minimalną ilość niezbędnych informacji, takich jak unikalny identyfikator lub inna forma identyfikatora:

// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))

Złożone obiekty powinny być przechowywane jako dane w jednym źródle prawdy, np. w warstwie danych. Po dotarciu do miejsca docelowego możesz pobrać wymagane informacje z jednego źródła informacji za pomocą przekazanego identyfikatora. Aby pobrać argumenty w funkcji ViewModel, która odpowiada za dostęp do warstwy danych, użyj funkcji SavedStateHandle funkcji 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)

// …

}

Takie podejście pomaga zapobiegać utracie danych podczas zmian konfiguracji i niezgodnościom, gdy dany obiekt jest aktualizowany lub modyfikowany.

Pełniejsze wyjaśnienie, dlaczego nie należy przekazywać złożonych danych jako argumentów, oraz lista obsługiwanych typów argumentów znajdują się w artykule Przesyłanie danych między miejscami docelowymi.

Komponent Navigation Compose obsługuje precyzyjne linki, które można zdefiniować w ramach funkcji composable(). Parametr deepLinks przyjmuje listę obiektów NavDeepLink, które można szybko utworzyć za pomocą metody 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)
}

Te precyzyjne linki umożliwiają powiązanie określonego adresu URL, działania lub typu mime z komponowalnym elementem. Domyślnie te precyzyjne linki nie są widoczne dla aplikacji zewnętrznych. Aby udostępnić te precyzyjne linki na zewnątrz, musisz dodać odpowiednie elementy <intent-filter> do pliku manifest.xml aplikacji. Aby włączyć precyzyjny link w poprzednim przykładzie, w elemencie <activity> w pliku manifestu dodaj:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

Nawigacja automatycznie tworzy precyzyjny link do tej składowej, gdy precyzyjny link zostanie wywołany przez inną aplikację.

Tych samych precyzyjnych linków możesz też używać do tworzenia PendingIntent z odpowiednim precyzyjnym linkiem z komponowalnego:

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)
}

Możesz potem użyć tego deepLinkPendingIntent jak każdego innego PendingIntent, aby otworzyć aplikację w miejscu docelowym precyzyjnego linku.

Nawigacja zagnieżdżona

Informacje o tworzeniu zagnieżdżonych diagramów nawigacji znajdziesz w artykule Zagnieżdżone diagramy.

Integracja z dolnym paskiem nawigacji

Definiując NavController na wyższym poziomie hierarchii komponentów, możesz połączyć komponent Nawigacja z innymi komponentami, takimi jak komponent dolnej nawigacji. Dzięki temu możesz się poruszać, klikając ikony na dolnym pasku.

Aby używać komponentów BottomNavigationBottomNavigationItem, dodaj do aplikacji na Androida zależność androidx.compose.material.

Groovy

dependencies {
    implementation "androidx.compose.material:material:1.7.6"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.7.6")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Aby połączyć elementy na dolnym pasku nawigacji z trasami na diagramie nawigacji, zalecamy zdefiniowanie klasy, takiej jak TopLevelRoute, która ma klasę trasy i ikonę.

data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)

Następnie umieść te trasy na liście, której mogą używać BottomNavigationItem:

val topLevelRoutes = listOf(
   TopLevelRoute("Profile", Profile, Icons.Profile),
   TopLevelRoute("Friends", Friends, Icons.Friends)
)

W komponentach BottomNavigation pobieraj bieżącą wartość NavBackStackEntry za pomocą funkcji currentBackStackEntryAsState(). Ta pozycja zapewnia dostęp do bieżącej wersji NavDestination. Wybrany stan każdego elementu BottomNavigationItem można następnie określić, porównując trasę elementu z trasą bieżącego miejsca docelowego i jego nadrzędnych miejsc docelowych, aby obsłużyć przypadki, gdy korzystasz z zagnieżdżonej nawigacji za pomocą hierarchii NavDestination.

Trasa elementu jest też używana do łączenia funkcji lambda onClick z wywołaniem funkcji navigate, dzięki czemu kliknięcie elementu powoduje przejście do tego elementu. Dzięki flagom saveStaterestoreState stan tego elementu i stos elementów do cofnięcia są prawidłowo zapisywane i przywracane podczas przełączania się między elementami nawigacji u dołu ekranu.

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(...) }
  }
}

Tutaj korzystasz z metody NavController.currentBackStackEntryAsState(), aby wyodrębnić stan navController z funkcji NavHost i udostępnić go komponentowi BottomNavigation. Oznacza to, że BottomNavigation automatycznie ma najbardziej aktualny stan.

Interoperacyjność

Jeśli chcesz użyć komponentu Nawigacja w komponencie Compose, masz 2 możliwości:

  • Zdefiniuj graf nawigacji za pomocą komponentu Nawigacja dla fragmentów.
  • Zdefiniuj graf nawigacji za pomocą elementu NavHost w sekcji Compose (Tworzenie) przy użyciu miejsc docelowych Compose. Jest to możliwe tylko wtedy, gdy wszystkie ekrany w diagramie nawigacji są składanymi elementami.

Dlatego w przypadku aplikacji mieszanych, które łączą komponenty Compose i View, zalecamy używanie komponentu nawigacji opartej na fragmentach. Fragmenty będą zawierać ekrany oparte na widokach, ekrany Compose oraz ekrany, które korzystają zarówno z widoków, jak i Compose. Gdy zawartość każdego fragmentu znajdzie się w Compose, należy połączyć wszystkie te ekrany z Compose nawigacji i usunąć wszystkie fragmenty.

Aby zmienić miejsca docelowe w kodzie usługi Compose, musisz ujawnić zdarzenia, które mogą być przekazywane do dowolnego komponentu w hierarchii i mogą być przez niego wywoływane:

@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

W fragmentie łączysz komponent Nawigacja oparty na fragmencie z komponentem Compose. Aby to zrobić, znajdź element NavController i przejdź do miejsca docelowego:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

Możesz też przekazać NavController w hierarchii Compose. Jednak udostępnianie prostych funkcji jest znacznie bardziej przydatne i łatwe do przetestowania.

Testowanie

Odłącz kod nawigacji od miejsc docelowych w komponowanych treściach, aby umożliwić testowanie poszczególnych treści w komponowanych treściach oddzielnie od NavHost.

Oznacza to, że nie należy przekazywać navController bezpośrednio do żadnej funkcji kompozytowej, a zamiast tego należy przekazywać wywołania zwrotne nawigacji jako parametry. Dzięki temu wszystkie komponenty można testować osobno, ponieważ nie wymagają one instancji navController w testach.

Poziom pośrednictwa zapewniany przez funkcję lambda composable pozwala na rozdzielenie kodu nawigacji od samego komponentu. Funkcja działa w 2 kierunkach:

  • Przekazywanie do kompozytowalnej funkcji tylko przeanalizowanych argumentów
  • Przekazywanie funkcji lambda, które powinny być wywoływane przez komponent, do nawigacji, a nie do NavController.

Na przykład kompozyt ProfileScreen, który przyjmuje jako dane wejściowe userId i pozwala użytkownikom przejść na stronę profilu znajomego, może mieć podpis:

@Composable
fun ProfileScreen(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 
}

Dzięki temu komponent ProfileScreen działa niezależnie od nawigacji, co pozwala na jego niezależne testowanie. Funkcja lambda composable ujęłaby minimalną logikę potrzebną do połączenia interfejsów API nawigacji z komponowalnym:

@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))
    }
}

Zalecamy napisanie testów, które obejmują wymagania dotyczące nawigacji w aplikacji. W tym celu przetestuj NavHost, czyli czynności nawigacyjne przekazywane do komponentów, a także poszczególne komponenty ekranu.

Testowanie NavHost

Aby rozpocząć testowanie NavHost , dodaj tę zależność testowania nawigacji:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

Zawiń funkcję NavHost w komponent, który przyjmuje jako parametr wartość NavHostController.

@Composable
fun AppNavHost(navController: NavHostController){
  NavHost(navController = navController){ ... }
}

Teraz możesz przetestować AppNavHost i całą logikę nawigacji zdefiniowaną w NavHost, przekazując instancję artefaktu testowania nawigacji TestNavHostController. Test UI, który sprawdza miejsce docelowe na początku NavHost, wygląda tak:

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()
    }
}

Testowanie działań związanych z nawigacją

Implementację nawigacji możesz testować na kilka sposobów: klikając elementy interfejsu, a potem weryfikując wyświetlane miejsce docelowe lub porównując oczekiwaną trasę z obecną.

Ponieważ chcesz przetestować implementację konkretnej aplikacji, kliknięcia w interfejsie są preferowane. Aby dowiedzieć się, jak testować te funkcje razem z poszczególnymi funkcjami składanymi, zapoznaj się z tym testem w Jetpack Compose w Codelab.

Możesz też użyć navController, aby sprawdzić swoje twierdzenia, porównując bieżącą trasę z oczekiwaną, korzystając z funkcji navController:currentBackStackEntry

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}

Więcej wskazówek dotyczących podstaw testowania w Compose znajdziesz w artykule Testowanie układu Compose i w codelab Testowanie w Jetpack Compose. Więcej informacji o zaawansowanym testowaniu kodu nawigacji znajdziesz w przewodniku Testowanie nawigacji.

Więcej informacji

Więcej informacji o komponencie Nawigacja w Jetpacku znajdziesz w artykule Pierwsze kroki z komponentem Nawigacja lub w laboratorium kodu Nawigacja w Jetpack Compose.

Aby dowiedzieć się, jak zaprojektować nawigację w aplikacji, aby dostosowywała się do różnych rozmiarów, orientacji i formatów ekranu, przeczytaj artykuł Nawigacja w elastycznym interfejsie użytkownika.

Aby dowiedzieć się więcej o bardziej zaawansowanej implementacji nawigacji w Compose w ramach aplikacji modułowej, w tym o takich koncepcjach jak zintegrowane wykresy i pasek nawigacji u dołu ekranu, zapoznaj się z aplikacją Now in Android na GitHubie.

Próbki