Nawigacja przy tworzeniu wiadomości

Komponent Nawigacja zapewnia obsługę aplikacji Jetpack Compose. Można się między nimi poruszać, korzystając z infrastruktury i funkcji komponentu Nawigacja.

Skonfiguruj

Aby umożliwić tworzenie, użyj tej zależności w pliku build.gradle modułu aplikacji:

Groovy

dependencies {
    def nav_version = "2.7.7"

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

Kotlin

dependencies {
    val nav_version = "2.7.7"

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

Rozpocznij

Implementując nawigację w aplikacji, zaimplementuj host, wykres i kontroler nawigacji. Więcej informacji znajdziesz w sekcji Nawigacja.

Informacje o tym, jak utworzyć element NavController w sekcji Utwórz, znajdziesz w sekcji Tworzenie w artykule Tworzenie kontrolera nawigacji.

Tworzenie hosta NavHost

Informacje o tym, jak utworzyć obiekt NavHost w sekcji Utwórz, znajdziesz w sekcji „Tworzenie” w artykule Projektowanie wykresu nawigacyjnego.

Informacje o tym, jak przejść do elementu kompozycyjnego, znajdziesz w sekcji Przechodzenie do miejsca docelowego w dokumentacji architektury.

Nawigacja oraz tworzenie wiadomości obsługuje też przekazywanie argumentów między kompozycyjnymi miejscami docelowymi. Aby to zrobić, musisz dodać do trasy obiekty zastępcze argumentów, podobnie jak w przypadku dodawania argumentów do precyzyjnego linku przy użyciu podstawowej biblioteki nawigacji:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

Domyślnie wszystkie argumenty są analizowane jako ciągi znaków. Parametr arguments composable() może mieć listę obiektów NamedNavArgument. Możesz szybko utworzyć obiekt NamedNavArgument za pomocą metody navArgument(), a potem określić jego dokładną wartość type:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

Argumenty należy wyodrębnić z funkcji NavBackStackEntry, która jest dostępna w ramach lambda funkcji composable().

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Aby przekazać argument do miejsca docelowego, musisz dodać go do trasy podczas wywoływania navigate:

navController.navigate("profile/user1234")

Listę obsługiwanych typów znajdziesz w artykule Przekazywanie danych między miejscami docelowymi.

Pobieranie złożonych danych podczas nawigacji

Zdecydowanie odradzamy przekazywanie złożonych obiektów danych podczas nawigacji. Zamiast tego przekazuj tylko minimalną liczbę niezbędnych informacji, takich jak unikalny identyfikator lub inna forma identyfikatora, jako argumenty podczas wykonywania działań nawigacyjnych:

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

Złożone obiekty powinny być przechowywane w jednym źródle danych, takim jak warstwa danych. Po dotarciu do miejsca docelowego możesz wczytać wymagane informacje z jednego źródła, korzystając z przekazanego identyfikatora. Aby pobrać w komponencie ViewModel argumenty odpowiedzialne za dostęp do warstwy danych, użyj funkcji ViewModel SavedStateHandle:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

To podejście pomaga zapobiec utracie danych podczas zmian konfiguracji i niespójnościom, gdy dany obiekt jest aktualizowany lub zmieniany.

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

Dodaj opcjonalne argumenty

Nawigacja oraz tworzenie wiadomości obsługuje opcjonalne argumenty nawigacji. Argumenty opcjonalne różnią się od wymaganych argumentów na 2 sposoby:

  • Należy je uwzględnić z zastosowaniem składni parametrów zapytania ("?argName={argName}")
  • Muszą mieć ustawioną wartość defaultValue lub zawierać nullable = true (co domyślnie ustawia wartość domyślną na null)

Oznacza to, że wszystkie argumenty opcjonalne muszą być jawnie dodane do funkcji composable() w formie listy:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Teraz nawet jeśli do miejsca docelowego nie jest przekazywany żaden argument, zamiast niego używany jest defaultValue „user1234”.

Struktura obsługi argumentów w ramach tras sprawia, że obiekty kompozycyjne są całkowicie niezależne od nawigacji i znacznie ułatwia testowanie.

Tworzenie wiadomości w nawigacji obsługuje niejawne precyzyjne linki, które można też zdefiniować w ramach funkcji composable(). Jego parametr deepLinks akceptuje listę obiektów NavDeepLink, które można szybko utworzyć za pomocą metody navDeepLink():

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

Te precyzyjne linki umożliwiają powiązanie określonego adresu URL, działania lub typu MIME z funkcją kompozycyjną. Domyślnie 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, dodaj ten fragment w elemencie <activity> pliku manifestu:

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

Nawigowanie automatycznie za pomocą precyzyjnych linków do tej funkcji kompozycyjnej, gdy precyzyjny link zostanie aktywowany przez inną aplikację.

Tych samych precyzyjnych linków można też użyć do utworzenia elementu PendingIntent z odpowiednim precyzyjnym linkiem z komponentu:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

Następnie możesz użyć tego elementu deepLinkPendingIntent jak każdego innego elementu PendingIntent, aby otwierać aplikację pod miejscem docelowym precyzyjnego linku.

Nawigacja zagnieżdżona

Więcej informacji o tworzeniu zagnieżdżonych wykresów nawigacji znajdziesz w artykule Wykresy zagnieżdżone.

Integracja z dolnym paskiem nawigacyjnym

Jeśli zdefiniujesz element NavController na wyższym poziomie w hierarchii kompozycyjnej, możesz połączyć Nawigację z innymi komponentami, takimi jak dolny komponent nawigacyjny. Dzięki temu możesz się poruszać, klikając ikony na dolnym pasku.

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

Groovy

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

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.12"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

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

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.12"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Aby połączyć elementy na dolnym pasku nawigacyjnym z trasami na wykresie nawigacyjnym, zalecamy zdefiniowanie zamkniętej klasy, np. Screen widocznej tutaj, która zawiera trasę i identyfikator zasobu ciągu znaków dla miejsc docelowych.

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

Następnie umieść te elementy na liście, której może używać BottomNavigationItem:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

W BottomNavigation funkcji kompozycyjnej pobierz bieżący NavBackStackEntry za pomocą funkcji currentBackStackEntryAsState(). Ten wpis zapewnia dostęp do bieżącego zasobu 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 z myślą o obsłudze zgłoszeń w przypadku korzystania z nawigacji zagnieżdżonej za pomocą hierarchii NavDestination.

Trasa elementu jest też używana do połączenia lambda onClick z wywołaniem navigate, aby kliknięcie elementu powodowało przejście do niego. Dzięki flagom saveState i restoreState stan i stos wsteczny elementu są prawidłowo zapisywane i przywracane przy przełączaniu się między dolnymi elementami nawigacyjnymi.

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.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 = Screen.Profile.route, Modifier.padding(innerPadding)) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { FriendsList(navController) }
  }
}

Za pomocą metody NavController.currentBackStackEntryAsState() pobierasz stan navController z funkcji NavHost i udostępniasz go komponentowi BottomNavigation. Oznacza to, że BottomNavigation będzie automatycznie mieć najbardziej aktualny stan.

Wpisz zasadę bezpieczeństwa w nawigacji – tworzenie wiadomości

Kod na tej stronie nie nadaje się do bezpiecznego typu. Możesz wywołać funkcję navigate() z istniejącymi trasami lub nieprawidłowymi argumentami. Możesz jednak taką strukturę kodu nawigacji można było zastosować w czasie działania. Dzięki temu możesz uniknąć awarii i zadbać o to, aby:

  • Argumenty podawane podczas przechodzenia do wykresu docelowego lub nawigacyjnego są właściwe i że występują wszystkie wymagane argumenty.
  • Argumenty pobierane z funkcji SavedStateHandle są poprawnymi typami.

Więcej informacji na ten temat znajdziesz w artykule Bezpieczeństwo wpisywania w Kotlin DSL i w nawigacji tworzenia wiadomości.

Interoperacyjność

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

  • Zdefiniuj wykres nawigacyjny za pomocą komponentu Nawigacja dla fragmentów.
  • Zdefiniuj wykres nawigacyjny z elementem NavHost w sekcji Utwórz za pomocą miejsc docelowych tworzenia wiadomości. Jest to możliwe tylko wtedy, gdy wszystkie ekrany na wykresie nawigacyjnym są kompozycyjne.

W przypadku aplikacji z mieszaną funkcją tworzenia i widoku zalecamy użycie komponentu nawigacji opartej na fragmentach. W tych fragmentach będą umieszczane ekrany oparte na widoku danych, ekrany tworzenia i ekrany, które korzystają zarówno z widoków danych, jak i z komponowania. Gdy zawartość każdego fragmentu znajduje się w komponencie Utwórz, kolejnym krokiem jest powiązanie wszystkich tych ekranów z funkcją nawigacji Compose i usunięcie wszystkich fragmentów.

Aby zmienić miejsca docelowe w kodzie tworzenia, musisz ujawnić zdarzenia, które mogą być przekazywane i wywoływane przez dowolny element kompozycyjny w hierarchii:

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

We fragmencie tworzysz most między tworzeniem a komponentem Nawigacja opartym na fragmentach. Aby to zrobić, znajdź obiekt NavController i przejdź do miejsca docelowego:

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

Możesz też przekazać uprawnienie NavController w dół hierarchii tworzenia wiadomości. Ujawnianie prostych funkcji jest jednak o wiele więcej wielokrotnego użytku i łatwiejszego testowania.

Testowanie

Odłącz kod nawigacyjny od miejsc docelowych kompozycyjnych, aby umożliwić testowanie każdego elementu kompozycyjnego oddzielnie, niezależnie od funkcji kompozycyjnej NavHost.

Oznacza to, że nie należy przekazywać obiektu navController bezpośrednio do funkcji kompozycyjnej, tylko przekazywać wywołania zwrotne nawigacji jako parametry. Dzięki temu wszystkie kompozycje mogą być testowane indywidualnie, ponieważ nie wymagają one w testach wystąpienia elementu navController.

Poziom pośredniego parametru composable pozwala oddzielić kod nawigacyjny od elementu kompozycyjnego. Działa to w 2 kierunkach:

  • Przekazywanie do funkcji kompozycyjnej tylko przeanalizowanych argumentów
  • Przekazuj lambda, które powinny być aktywowane przez funkcję kompozycyjną, aby nawigować, a nie sam obiekt NavController.

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

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

W ten sposób funkcja kompozycyjna Profile będzie działać niezależnie od Nawigacji, co umożliwi jej niezależne testowanie. Lambda composable uwzględniłaby minimalną logikę potrzebną do wypełnienia luki między interfejsami API nawigacji a elementem kompozycyjnym:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

Zalecamy opracowanie testów, które spełnią wymagania dotyczące nawigacji po aplikacji. W tym celu należy przetestować interfejs NavHost, działania nawigacyjne przekazywane do funkcji kompozycyjnych oraz poszczególnych elementów kompozycyjnych na ekranie.

Testowanie: NavHost

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

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

Możesz skonfigurować obiekt testowy NavHost i przekazać do niego wystąpienie instancji navController. W tym celu artefakt testowania nawigacji udostępnia TestNavHostController. Tak wygląda test interfejsu, który sprawdza początkową lokalizację docelową aplikacji i 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()
    }
}

Testowanie działań nawigacyjnych

Implementację nawigacji możesz przetestować na wiele sposobów, klikając elementy interfejsu, a potem sprawdzając wyświetlane miejsce docelowe lub porównując oczekiwaną trasę z aktualną trasą.

Jeśli chcesz przetestować implementację konkretnej aplikacji, preferowane są kliknięcia interfejsu. Aby dowiedzieć się, jak przetestować tę funkcję w połączeniu z osobnymi funkcjami kompozycyjnymi, zapoznaj się z ćwiczeniem w Codelabs dotyczącym testowania w Jetpack Compose.

Możesz też użyć metody navController, aby sprawdzić asercje, porównując bieżącą trasę ciągu znaków z tą oczekiwaną, używając metody currentBackStackEntry navController:

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

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

Więcej wskazówek dotyczących podstaw testowania w usłudze Compose znajdziesz w sekcjach Testowanie układu tworzenia wiadomości i Testowanie w Jetpack Compose. Więcej informacji o zaawansowanym testowaniu kodu nawigacyjnego znajdziesz w przewodniku Testowanie nawigacji.

Więcej informacji

Aby dowiedzieć się więcej o Nawigacji w Jetpack, przeczytaj artykuł Wprowadzenie do komponentu Nawigacja lub weź udział w ćwiczeniach z programowania nawigacyjnych w Jetpack tego typu.

Aby dowiedzieć się, jak zaprojektować nawigację w aplikacji, aby dostosowywała się do różnych rozmiarów, orientacji i formatów ekranu, przeczytaj sekcję Nawigacja w elastycznych interfejsach.

Więcej informacji o bardziej zaawansowanej implementacji nawigacji w ramach tworzenia wiadomości w aplikacji modułowej, w tym o zagnieżdżonych wykresach i integracji z dolnym paskiem nawigacyjnym, znajdziesz w aplikacji Now in Android na GitHubie.

Próbki