Kotlin DSL 및 Navigation Compose의 유형 안전성

이 페이지에는 Navigation Kotlin DSLNavigation Compose에 런타임 유형 안전성을 제공하는 것과 관련된 권장사항이 포함되어 있습니다. 요약하자면, 앱의 각 화면 또는 탐색 그래프를 모듈별 탐색 파일에 매핑해야 한다는 내용입니다. 결과 파일에는 각각 지정된 대상의 모든 탐색 관련 정보가 포함되어야 합니다. 이러한 탐색 파일은 Kotlin 공개 상태 수정자가 런타임 유형 안전성을 제공하는 곳이기도 합니다.

  • 유형 안전성 함수는 코드베이스의 나머지 부분에 노출되는 공개 함수입니다.
  • 특정 화면 또는 탐색 그래프의 탐색 관련 개념은 동일한 파일에 함께 위치하며 비공개로 유지되어 코드베이스의 나머지 부분에서 액세스할 수 없습니다.

탐색 그래프 분할

탐색 그래프는 화면별로 분할해야 합니다. 이는 기본적으로 화면을 여러 구성 가능한 함수로 분할할 때와 동일한 접근 방식입니다. 각 화면에는 NavGraphBuilder 확장 함수가 있어야 합니다.

이 확장 함수는 스테이트리스(Stateless) 화면 수준의 구성 가능한 함수와 탐색별 로직 간의 가교 역할을 합니다. 이 레이어는 상태의 출처와 이벤트 처리 방법을 정의할 수도 있습니다.

다음은 다른 모듈에서 액세스할 수 없도록 자체 모듈에 대해 internal로 지정할 수 있는 일반적인 ConversationScreen입니다.

// ConversationScreen.kt

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

다음 NavGraphBuilder 확장 함수는 ConversationScreen 컴포저블을 NavGraph의 대상으로 추가합니다. 또한 화면 UI 상태를 제공하고 화면 관련 비즈니스 로직을 처리하는 ViewModel과 화면을 연결합니다. ViewModel에서 처리할 수 없는 탐색 이벤트는 호출자에게 노출됩니다.

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

ConversationNavigation.kt 파일은 탐색 라이브러리 코드를 대상 자체와 분리합니다. 또한 경로 또는 인수 ID와 같은 탐색 개념을 캡슐화하여 제공합니다. 이러한 개념은 이 파일 외부로 유출되면 안 되므로 비공개로 유지합니다. 이 레이어에서 처리할 수 없는 탐색 이벤트는 올바른 수준에서 처리되도록 호출자에게 노출해야 합니다. 위 코드 스니펫에서 onNavigateToParticipantList가 있는 이벤트의 예를 확인할 수 있습니다.

유형 안전성 탐색

Navigation Compose가 빌드된 Navigation Kotlin DSL현재 Safe Args가 제공하는 종류의 컴파일 시간 유형 안전성을 탐색 XML 리소스 파일에 빌드된 탐색 그래프에 제공하지 않습니다. Safe Args는 탐색 대상 및 작업을 위한 유형 안전 클래스와 메서드가 포함된 코드를 생성합니다. 그러나 런타임 시 유형 안정성을 갖도록 탐색 코드를 구성할 수 있습니다. 이를 통해 비정상 종료를 방지하고 다음을 확인할 수 있습니다.

  • 대상 또는 탐색 그래프로 이동할 때 제공하는 인수가 올바른 유형이고 필요한 인수가 모두 있습니다.
  • SavedStateHandle에서 가져오는 인수가 올바른 유형입니다.

또한 각 대상은 다른 대상이 안전하게 이동할 수 있도록 NavController 확장 함수를 노출해야 합니다.

// ConversationNavigation.kt

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

이동할 때 popUpTo, savedState, restoreState 또는 singleTop과 같이 다른 NavOptions가 포함된 앱 화면으로 이동하려면 NavController 확장 함수에 선택적 매개변수를 전달하세요.

// HomeNavigation.kt

const val HomeRoute = "home"

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

유형 안전성 인수 래퍼

필요에 따라 유형 안전성 래퍼를 만들어 ViewModel의 SavedStateHandle 및 대상 콘텐츠의 NavBackStackEntry에서 인수를 추출하여 이 섹션의 시작 부분에 언급된 이점을 누릴 수 있습니다.

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

탐색 그래프 조합

탐색 그래프는 위에서 설명한 유형의 안전성 확장 함수를 사용하여 대상을 추가하고 탐색합니다.

다음 예에서 다른 두 개의 대상(참여자 명단)과 함께 대화 대상은 다음과 같이 앱 수준의 NavHost에 포함됩니다.

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

중첩된 탐색 그래프의 유형 안전성

여러 화면을 제공하는 모듈에 적합한 공개 상태를 선택해야 합니다. 이는 위 섹션의 각 메서드와 동일한 개념입니다. 그러나 다른 모듈에 개별 화면을 완전히 노출하는 것은 큰 의미가 없습니다. 이 경우에는 더 크고 독립적인 흐름의 일부로 취급해야 합니다.

이러한 독립적인 화면 집합을 중첩된 탐색 그래프라고 합니다. 이를 통해 단일 NavGraphBuilder 확장 메서드에 여러 화면을 포함할 수 있습니다. 이 메서드는 이러한 NavController 확장 메서드를 사용하여 차례로 동일한 모듈 내의 화면을 함께 연결합니다.

다음 예에서 이전 섹션에 설명한 대화 대상은 다른 두 대상(대화 목록참여자 목록)과 함께 n 중첩된 탐색 그래프를 나타냅니다.

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

다음과 같이 앱 수준의 NavHost에서 여러 개의 중첩된 탐색 그래프를 사용할 수 있습니다.

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