Eingabesicherheit in Kotlin DSL und Navigation Compose

Auf dieser Seite finden Sie Best Practices zur Sicherheit von Laufzeittypen für Navigation Kotlin DSL und Navigation Compose. Sie sollten also jeden Bildschirm Ihrer App oder jedes Navigationsdiagramm einer Navigationsdatei für jedes Modul zuordnen. Die resultierenden Dateien sollten jeweils alle Navigationsinformationen für das jeweilige Ziel enthalten. In den folgenden Navigationsdateien bieten Kotlin-Sichtbarkeitsmodifikatoren auch Sicherheit für den Laufzeittyp:

  • Typsichere Funktionen sind für den Rest der Codebasis öffentlich verfügbar.
  • Navigationsspezifische Konzepte für einen bestimmten Bildschirm oder ein bestimmtes Navigationsdiagramm werden in derselben Datei gespeichert und geheim gehalten, damit sie für den Rest der Codebasis nicht zugänglich sind.

Navigationsgrafik teilen

Sie sollten die Navigationsgrafik nach Bildschirm aufteilen. Dies ist im Wesentlichen der gleiche Ansatz wie beim Teilen von Bildschirmen auf verschiedene zusammensetzbare Funktionen. Jeder Bildschirm sollte eine Erweiterungsfunktion NavGraphBuilder haben.

Diese Erweiterungsfunktion ist die Brücke zwischen einer zustandslosen zusammensetzbaren Funktion auf Bildschirmebene und einer navigationsspezifischen Logik. In dieser Ebene kann auch definiert werden, woher der Zustand stammt und wie Ereignisse verarbeitet werden.

Hier siehst du eine typische ConversationScreen, die als internal für ihr eigenes Modul verwendet werden kann, sodass andere Module nicht darauf zugreifen können:

// ConversationScreen.kt

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

Mit der folgenden NavGraphBuilder-Erweiterungsfunktion wird die zusammensetzbare Funktion ConversationScreen als Ziel dieser NavGraph hinzugefügt. Außerdem wird der Bildschirm mit einem ViewModel verbunden, das den Bildschirm-UI-Status bereitstellt und die bildschirmbezogene Geschäftslogik verarbeitet. Navigationsereignisse, die nicht von der ViewModel verarbeitet werden können, werden für den Aufrufer freigegeben.

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

Die Datei ConversationNavigation.kt trennt den Code von der Navigationsbibliothek vom Ziel selbst. Außerdem werden Navigationskonzepte wie Routen oder Argument-IDs gekapselt, die privat gehalten werden, da sie niemals außerhalb dieser Datei Datenlecks verursachen sollten. Navigationsereignisse, die auf dieser Ebene nicht verarbeitet werden können, müssen für den Aufrufer verfügbar gemacht werden, damit sie auf der richtigen Ebene verarbeitet werden. Im Code-Snippet oben finden Sie ein Beispiel für ein solches Ereignis mit onNavigateToParticipantList.

Sichere Navigation eingeben

Die Navigation Kotlin DSL, auf der Navigation Compose basiert, bietet aktuell keine Sicherheit für die Kompilierungszeit der Art, die Safe Args für Navigationsdiagramme in XML-Navigationsressourcendateien bietet. Safe Args generiert Code, der typsichere Klassen und Methoden für Navigationsziele und -aktionen enthält. Sie können Ihren Navigationscode jedoch so strukturieren, dass er während der Laufzeit typsicher ist. Sie können Abstürze vermeiden und Folgendes sicherstellen:

  • Die Argumente, die Sie beim Aufrufen eines Ziels oder einer Navigationsgrafik angeben, sind die richtigen Typen und dass alle erforderlichen Argumente vorhanden sind.
  • Die Argumente, die Sie von SavedStateHandle abrufen, sind die richtigen Typen.

Für jedes Ziel sollte auch eine NavController-Erweiterungsfunktion verfügbar sein, damit andere Ziele sicher dorthin gelangen können.

// ConversationNavigation.kt

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

Wenn Sie einen Bildschirm Ihrer App mit einem anderen NavOptions aufrufen möchten, z. B. popUpTo, savedState, restoreState oder singleTop, übergeben Sie einen optionalen Parameter an die NavController-Erweiterungsfunktion.

// HomeNavigation.kt

const val HomeRoute = "home"

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

Wrapper für sichere Argumente eingeben

Sie können optional einen typsicheren Wrapper erstellen, um die Argumente aus einem SavedStateHandle für Ihre ViewModel und aus einem NavBackStackEntry im Inhalt eines Ziels zu extrahieren, um die in der Einführung dieses Abschnitts genannten Vorteile nutzen zu können.

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

Navigationsdiagramm zusammenstellen

In Navigationsdiagrammen werden die oben beschriebenen typsicheren Erweiterungsfunktionen verwendet, um Ziele hinzuzufügen und zu ihnen zu navigieren.

Im folgenden Beispiel ist das Ziel conversation zusammen mit den beiden anderen Zielen home und Participant list in einer App-Ebene NavHost so enthalten:

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

Sicherheit in verschachtelten Navigationsgrafiken eingeben

Für Module mit mehreren Bildschirmen sollten Sie die richtige Sichtbarkeit auswählen. Das Konzept ist das gleiche wie bei jeder Methode in den obigen Abschnitten. Es ist jedoch möglicherweise nicht sinnvoll, einzelne Bildschirme überhaupt anderen Modulen zu präsentieren. In diesem Fall sollten Sie sie stattdessen als Teil eines größeren, eigenständigen Ablaufs behandeln.

Diese in sich geschlossene Gruppe von Bildschirmen wird als verschachteltes Navigationsdiagramm bezeichnet. So können Sie mehrere Bildschirme in eine einzige NavGraphBuilder-Erweiterungsmethode einbinden. Diese Methode verwendet diese NavController-Erweiterungsmethoden wiederum, um die Bildschirme innerhalb desselben Moduls miteinander zu verknüpfen.

Im folgenden Beispiel wird das in den vorherigen Abschnitten beschriebene Ziel conversation (Unterhaltungen) in einem verschachtelten Navigationsdiagramm neben zwei anderen Zielen, einer Unterhaltungsliste und einer Teilnehmerliste, angezeigt:

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

So können Sie mehrere verschachtelte Navigationsgrafiken in einem NavHost auf App-Ebene verwenden:

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