Wpisz zabezpieczenia w Kotlin DSL i w nawigacji – tworzenie

Ta strona zawiera sprawdzone metody zapewniania bezpieczeństwa typu środowiska wykonawczego w interfejsach Nawigacja Kotlin i Nawigacji w tworzeniu. Mówiąc ogólnie, zmapuj każdy ekran aplikacji lub wykres nawigacyjny na plik nawigacyjny z uwzględnieniem poszczególnych modułów. Oba pliki powinny zawierać wszystkie informacje związane z nawigacją w przypadku danego miejsca docelowego. W tych plikach nawigacyjnych znajdują się też modyfikatory widoczności Kotlin, które zapewniają bezpieczeństwo typu środowiska wykonawczego:

  • Funkcje bezpiecznego typu są publicznie dostępne dla pozostałej części bazy kodu.
  • Koncepcje związane z nawigacją po konkretnym ekranie lub wykresie nawigacyjnym są umieszczone we wspólnej lokalizacji i zapisane w tym samym pliku, dzięki czemu są niedostępne w pozostałej części bazy kodu.

Dzielenie wykresu nawigacyjnego

Wykres nawigacyjny należy podzielić według ekranu. Zasadniczo wygląda to tak samo jak w przypadku podziału ekranów na różne funkcje kompozycyjne. Każdy ekran powinien mieć funkcję rozszerzenia NavGraphBuilder.

Ta funkcja rozszerzenia łączy bezstanową funkcję kompozycyjną na poziomie ekranu a logiką właściwą do nawigacji. Może ona też określać, skąd pochodzi stan i jak są obsługiwane zdarzenia.

Oto typowy obiekt ConversationScreen, który może być komponentem internal w swoim module, tak aby inne moduły nie miały do niego dostępu:

// ConversationScreen.kt

@Composable
internal fun ConversationScreen(
  uiState: ConversationUiState,
  onPinConversation: () -> Unit,
  onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }

Poniższa funkcja rozszerzenia NavGraphBuilder dodaje element kompozycyjny ConversationScreen jako miejsce docelowe obiektu NavGraph. Łączy on też ekran z modelem ViewModel, który określa stan interfejsu ekranu i obsługuje logikę biznesową związaną z ekranem. Zdarzenia nawigacji, których nie można obsługiwać przez ViewModel, są widoczne dla elementu wywołującego.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

// Adds conversation screen to `this` NavGraphBuilder
fun NavGraphBuilder.conversationScreen(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToParticipantList: (conversationId: String) -> Unit
) {
  composable("conversation/{$conversationIdArg}") {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ConversationViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ConversationScreen(
      uiState,
      ::viewModel.pinConversation,
      onNavigateToParticipantList
    )
  }
}

Plik ConversationNavigation.kt oddziela kod od biblioteki nawigacji od samego miejsca docelowego. Zapewnia też herbatę wokół koncepcji nawigacji, takich jak trasy czy identyfikatory argumentów, które są prywatne, ponieważ nie powinny nigdy wyciekać poza ten plik. Zdarzenia nawigacji, których nie można obsługiwać na tej warstwie, muszą być widoczne dla elementu wywołującego, aby były obsługiwane na odpowiednim poziomie. Przykład takiego zdarzenia znajdziesz we fragmencie kodu powyżej z parametrem onNavigateToParticipantList.

Wpisz bezpieczną nawigację

Nawigacja Kotlin DSL, na której opiera się usługa Navigation Compose, obecnie nie oferuje bezpieczeństwa typu w czasie kompilacji w rodzaju Safe Args, które są dostępne na wykresach nawigacyjnych utworzonych w plikach XML zasobów nawigacyjnych. Safe Args generuje kod zawierający klasy i metody bezpieczne dla miejsc docelowych i działań nawigacji. Możesz jednak uporządkować kod nawigacji w taki sposób, by był bezpieczny w czasie działania. Dzięki temu unikniesz awarii i zadbasz o to, aby:

  • Argumenty podawane podczas przechodzenia do wykresu docelowego lub nawigacyjnego są poprawne i zawierają wszystkie wymagane argumenty.
  • Argumenty pobierane z funkcji SavedStateHandle są poprawne.

Każde miejsce docelowe powinno też udostępniać funkcję rozszerzenia NavController, aby umożliwić innym miejscom docelowym bezpieczną nawigację do tego miejsca.

// ConversationNavigation.kt

fun NavController.navigateToConversation(conversationId: String) {
    this.navigate("conversation/$conversationId")
}

Jeśli podczas nawigacji chcesz przejść na ekran aplikacji z innymi elementami NavOptions, np. popUpTo, savedState, restoreState lub singleTop, przekaż opcjonalny parametr do funkcji rozszerzenia NavController.

// HomeNavigation.kt

const val HomeRoute = "home"

fun NavController.navigateToHome(navOptions: NavOptions? = null) {
    this.navigate(HomeRoute, navOptions)
}

Wpisz opakowanie bezpiecznych argumentów

Opcjonalnie możesz utworzyć bezpieczny typ opakowania, aby wyodrębnić argumenty z SavedStateHandle modelu ViewModel i z NavBackStackEntry w treści miejsca docelowego. Pozwoli to uzyskać korzyści wymienione we wstępie do tej sekcji.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

internal class ConversationArgs(val conversationId: String) {
  constructor(savedStateHandle: SavedStateHandle) :
    this(checkNotNull(savedStateHandle[conversationIdArg]) as String)
}

// ConversationViewModel.kt

internal class ConversationViewModel(...,
  savedStateHandle: SavedStateHandle
) : ViewModel() {
  private val conversationArgs = ConversationArgs(savedStateHandle)
}

Zbuduj wykres nawigacyjny

Wykresy nawigacyjne korzystają z opisanych powyżej funkcji bezpiecznych dla użytkowników rozszerzeń, aby dodawać miejsca docelowe i wyznaczać do nich nawigację.

W tym przykładzie miejsce docelowe rozmowy wraz z 2 innymi miejscami docelowymi (dom i listą uczestników) znajduje się na poziomie aplikacji NavHost w ten sposób:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeRoute,
    modifier = modifier
  ) {

    homeScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )

    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    }

    participantListScreen()
}

Określanie bezpieczeństwa na zagnieżdżonych wykresach nawigacji

Wybierz odpowiednią widoczność modułów, które mają kilka ekranów. Obowiązują te same zasady, co w przypadku każdej metody opisanej w powyższych sekcjach. Czasami jednak nie ma sensu prezentowanie poszczególnych ekranów innym modułom. W takim przypadku potraktuj je jako część większej, niezależnej przestrzeni.

Ten samodzielny zestaw ekranów jest nazywany zagnieżdżonym wykresem nawigacyjnym. Dzięki temu możesz uwzględnić wiele ekranów w pojedynczej metodzie rozszerzenia NavGraphBuilder. Ta metoda używa z kolei tych NavControllermetod rozszerzeń, aby łączyć ze sobą ekrany w tym samym module.

W poniższym przykładzie miejsce docelowe rozmowy opisane w poprzednich sekcjach jest wyświetlane jako zagnieżdżony wykres nawigacyjny położony obok 2 innych miejsc docelowych: listy rozmów i listy uczestników:

// ConversationGraphNavigation.kt

private val ConversationGraphRoutePattern = "conversation"

fun NavController.navigateToConversationGraph(navOptions: NavOptions? = null) {
  this.navigate(ConversationGraphRoutePattern, navOptions)
}

fun NavGraphBuilder.conversationGraph(navController: NavController) {
  navigation(
    startDestination = ConversationListRoutePattern,
    route = ConversationGraphRoutePattern
  ) {
    conversationListScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )
    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    )
    partipantList()
}

Na poziomie aplikacji NavHost możesz używać wielu zagnieżdżonych wykresów nawigacyjnych w ten sposób:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeGraphRoutePattern
    modifier = modifier
  ) {
    homeGraph(
      navController,
      onNavigateToConversation = {
        navController.navigateToConversationGraph()
      }
    }
    conversationGraph(navController)
}