Nawigacja przy tworzeniu wiadomości

Komponent nawigacji obsługuje aplikacje Jetpack Compose. Możesz przechodzić między funkcjami kompozycyjnymi, korzystając z infrastruktury i funkcji komponentu Navigation.

Najnowszą bibliotekę nawigacji w wersji alfa, stworzoną specjalnie z myślą o Compose, znajdziesz w dokumentacji Navigation 3.

Konfiguracja

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

Groovy

dependencies {
    def nav_version = "2.9.5"

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

Kotlin

dependencies {
    val nav_version = "2.9.5"

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

Rozpocznij

Podczas wdrażania nawigacji w aplikacji zaimplementuj hosta, wykres i kontroler nawigacji. Więcej informacji znajdziesz w omówieniu Nawigacji.

Informacje o tworzeniu NavController w Compose znajdziesz w sekcji Compose w artykule Tworzenie kontrolera nawigacji.

Tworzenie elementu NavHost

Informacje o tym, jak utworzyć NavHost w funkcji Compose, znajdziesz w sekcji Compose w artykule Projektowanie wykresu nawigacji.

Informacje o przechodzeniu do funkcji Composable znajdziesz w sekcji Przechodzenie do miejsca docelowego w dokumentacji architektury.

Informacje o przekazywaniu argumentów między miejscami docelowymi w funkcjach kompozycyjnych znajdziesz w sekcji poświęconej Compose w artykule Projektowanie wykresu nawigacji.

Pobieranie złożonych danych podczas nawigacji

Podczas nawigacji zdecydowanie nie zaleca się przekazywania złożonych obiektów danych. Zamiast tego podczas wykonywania działań nawigacyjnych należy przekazywać jako argumenty minimum 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 informacji, np. w warstwie danych. Po dotarciu do miejsca docelowego możesz za pomocą przekazanego identyfikatora wczytać wymagane informacje z jednego źródła danych. Aby pobrać argumenty w ViewModel, który odpowiada za dostęp do warstwy danych, użyj SavedStateHandleViewModel:

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 wprowadzania zmian w konfiguracji i wszelkim niespójnościom podczas aktualizowania lub modyfikowania danego obiektu.

Bardziej szczegółowe wyjaśnienie, dlaczego należy unikać przekazywania złożonych danych jako argumentów, oraz listę obsługiwanych typów argumentów znajdziesz w artykule Przekazywanie danych między miejscami docelowymi.

Navigation Compose obsługuje precyzyjne linki, które można zdefiniować w ramach funkcji composable(). Jego parametr deepLinks akceptuje 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 komponentem. Domyślnie te precyzyjne linki nie są udostępniane aplikacjom zewnętrznym. 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 przykładzie powyżej, dodaj w elemencie <activity> pliku manifestu ten kod:

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

Gdy precyzyjny link zostanie wywołany przez inną aplikację, nawigacja automatycznie przekieruje do tego komponentu.

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

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 użyć tego deepLinkPendingIntent tak jak każdego innego PendingIntent, aby otworzyć aplikację w miejscu docelowym precyzyjnego linku.

Zagnieżdżona nawigacja

Informacje o tworzeniu zagnieżdżonych wykresów nawigacji znajdziesz w artykule Zagnieżdżone wykresy.

Tworzenie adaptacyjnego dolnego paska nawigacyjnego i paska nawigacyjnego

NavigationSuiteScaffold wyświetla odpowiedni interfejs nawigacji w zależności od WindowSizeClass, w którym jest renderowana aplikacja. Na małych ekranach NavigationSuiteScaffold wyświetla pasek nawigacyjny u dołu, a na większych ekranach – panel nawigacyjny.

Więcej informacji znajdziesz w artykule Tworzenie adaptacyjnej nawigacji.

Interoperacyjność

Jeśli chcesz używać komponentu Navigation z Compose, masz 2 możliwości:

  • Zdefiniuj graf nawigacji za pomocą komponentu Navigation dla fragmentów.
  • Zdefiniuj wykres nawigacji za pomocą NavHost w Compose, korzystając z miejsc docelowych Compose. Jest to możliwe tylko wtedy, gdy wszystkie ekrany w grafie nawigacji są funkcjami kompozycyjnymi.

Dlatego w przypadku aplikacji, które korzystają zarówno z Compose, jak i z widoków, zalecamy używanie komponentu nawigacji opartej na fragmentach. Fragmenty będą wtedy zawierać ekrany oparte na widokach, ekrany Compose i ekrany, które korzystają zarówno z widoków, jak i z Compose. Gdy zawartość każdego fragmentu znajdzie się w Compose, kolejnym krokiem jest połączenie wszystkich ekranów za pomocą Navigation Compose i usunięcie wszystkich fragmentów.

Aby zmienić miejsca docelowe w kodzie Compose, udostępniasz zdarzenia, które mogą być przekazywane do dowolnej funkcji kompozycyjnej w hierarchii i przez nią wywoływane:

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

W fragmencie łączysz Compose z komponentem Navigation opartym na fragmentach, wyszukując NavController i przechodząc do miejsca docelowego:

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

Możesz też przekazać NavController w dół hierarchii Compose. Udostępnianie prostych funkcji jest jednak bardziej przydatne i łatwiejsze do testowania.

Testowanie

Oddziel kod nawigacji od miejsc docelowych, które można komponować, aby umożliwić testowanie każdego z nich osobno, niezależnie od funkcji NavHost.

Oznacza to, że nie należy przekazywać navController bezpośrednio do żadnego komponentu, tylko przekazywać wywołania zwrotne nawigacji jako parametry. Dzięki temu wszystkie funkcje kompozycyjne można testować indywidualnie, ponieważ nie wymagają one w testach instancji navController.

Poziom pośredniości zapewniany przez lambdę composable umożliwia oddzielenie kodu Navigation od samego komponentu. Działa to w 2 kierunkach:

  • Przekazuj do funkcji kompozycyjnej tylko przeanalizowane argumenty
  • Przekazuj lambdy, które powinny być wywoływane przez komponent kompozycyjny w celu nawigacji, zamiast samego NavController.

Na przykład funkcja kompozycyjna ProfileScreen, która przyjmuje jako dane wejściowe userId i umożliwia użytkownikom przejście na stronę profilu znajomego, może mieć następujący podpis:

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

Dzięki temu ProfileScreen komponent działa niezależnie od Navigation, co umożliwia jego niezależne testowanie. Funkcja lambda composable zawierałaby minimalną logikę potrzebną do połączenia interfejsów API Navigation z komponentem kompozycyjnym:

@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 pisanie testów, które obejmują wymagania dotyczące nawigacji w aplikacji, poprzez testowanie NavHost, działań nawigacyjnych przekazywanych do funkcji kompozycyjnych, a także poszczególnych funkcji kompozycyjnych ekranu.

Testowanie NavHost

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

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

Umieść NavHost w aplikacji w funkcji kompozycyjnej, która przyjmuje NavHostController jako parametr.

@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 interfejsu, który weryfikuje miejsce docelowe uruchomienia aplikacji i NavHost, może 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 przetestować na kilka sposobów, klikając elementy interfejsu, a następnie weryfikując wyświetlane miejsce docelowe lub porównując oczekiwaną trasę z bieżącą.

Ponieważ chcesz przetestować implementację konkretnej aplikacji, preferowane są kliknięcia interfejsu. Aby dowiedzieć się, jak testować poszczególne funkcje kompozycyjne w izolacji, zapoznaj się z samouczkiem dotyczącym testowania w Jetpack Compose.

Możesz też użyć navController, aby sprawdzić asercje, porównując bieżącą trasę z oczekiwaną za pomocą navController:currentBackStackEntry

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

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

Więcej informacji o podstawach testowania w Compose znajdziesz w artykule Testowanie układu Compose i w samouczku Testowanie w Jetpack Compose. Więcej informacji o zaawansowanym testowaniu kodu nawigacji znajdziesz w przewodniku Testowanie nawigacji.

Więcej informacji

Więcej informacji o nawigacji Jetpack znajdziesz w artykule Pierwsze kroki z komponentem Navigation lub w samouczku dotyczącym nawigacji w Jetpack Compose.

Aby dowiedzieć się, jak zaprojektować nawigację w aplikacji, która dostosowuje się do różnych rozmiarów ekranu, orientacji i rodzajów urządzeń, przeczytaj artykuł Nawigacja w elastycznych interfejsach.

Aby dowiedzieć się więcej o bardziej zaawansowanej implementacji nawigacji w Compose w aplikacji modułowej, w tym o koncepcjach takich jak zagnieżdżone wykresy i integracja paska nawigacyjnego u dołu, zapoznaj się z aplikacją Now in Android na GitHubie.

Próbki