결과 반환

Navigation 3 1.2.0부터 ResultEventBus API를 사용하여 대상에서 결과를 반환할 수 있습니다.

ResultEventBus는 두 가지 통신 모델을 제공합니다.

  • 이벤트 기반 결과: ResultEffect를 사용하여 일시적인 일회성 이벤트 (예: 확인 스낵바 표시 또는 부작용 트리거)
  • 상태 기반 결과: Compose가 conflateAsState를 사용하여 State로 최신 결과를 관찰합니다.

결과 이벤트 버스 설정

컴포저블 대상에서 ResultEventBus를 사용할 수 있도록 하려면 NavDisplay에 전달된 데코레이터 목록에 rememberResultEventBusNavEntryDecorator를 추가합니다. 이렇게 하면 각 대상의 콘텐츠에 LocalResultEventBus 컴포지션 로컬이 제공됩니다.

NavDisplay(
    /* ... */
    entryDecorators = listOf(
        rememberSaveableStateHolderNavEntryDecorator(),
        rememberResultEventBusNavEntryDecorator()
    )
)

결과 키

ResultEventBus는 키를 사용하여 각 결과를 식별하고 라우팅합니다. 전송자와 수신자는 동일한 키를 사용하여 결과를 일치시킵니다.

결과 키를 지정하는 방법은 두 가지입니다.

  • 명시적 키: 명시적 키 (예: resultKey = "pickup_address")를 지정할 수 있습니다. 일반적인 유형 (예: String, Boolean 또는 기본 유형)을 반환하거나 여러 대상에서 동일한 데이터 유형의 다른 인스턴스를 반환하는 경우 명시적 키를 사용합니다.
  • 유형 파생 키: 명시적 키를 지정하지 않으면 ResultEventBus에서 결과 유형의 toStringKClass (예: Contact::class.toString()) 표현을 사용하여 키를 자동으로 생성합니다. 고유한 도메인별 데이터 유형에 유형 파생 키를 사용하세요.

대상에서 결과 반환

화면 컴포저블을 재사용 가능하고 테스트 가능하게 유지하려면 화면 UI 내에서 LocalResultEventBus에 직접 액세스하지 마세요. 대신 화면에서 콜백 람다를 노출하세요. entryProvider에서 LocalResultEventBus.current를 사용하여 결과를 전송하고 뒤로 탐색하여 콜백을 처리합니다.

명시적 결과 키를 사용하여 결과를 전송할 수 있습니다.

import androidx.compose.runtime.Composable
import androidx.navigation3.runtime.result.LocalResultEventBus

entry<AddressPickerRoute> {
    val resultBus = LocalResultEventBus.current

    AddressPickerScreen(
        onAddressSelected = { selectedAddress: Address ->
            resultBus.sendResult(
                resultKey = "pickup_address",
                result = selectedAddress
            )
            navigator.goBack()
        }
    )
}

유형 파생 키를 사용하여 결과를 전송할 수도 있습니다.

import androidx.compose.runtime.Composable
import androidx.navigation3.runtime.result.LocalResultEventBus

entry<ContactPickerRoute> {
    val resultBus = LocalResultEventBus.current

    ContactPickerScreen(
        onContactSelected = { selectedContact: Contact ->
            resultBus.sendResult(result = selectedContact)
            navigator.goBack()
        }
    )
}

결과 수신

대상은 이벤트 기반 효과 또는 상태 기반 관찰 가능 항목을 사용하여 결과를 사용할 수 있습니다.

API 동작 권장 사용 사례
ResultEffect 대기열에 추가됨: 키에 대해 발생한 모든 결과를 순서대로 처리합니다. 일회성 이벤트 및 부수 효과 (예: 스낵바 표시 또는 ViewModel로 전달)
conflateAsState 혼합: 중간 결과를 삭제하고 최신 결과만 Compose State로 유지합니다. 경량 UI 상태 수정자 (예: 활성 필터 태그 또는 선택 재정의)

ResultEffect로 일회성 이벤트 처리

분석 트리거, 스낵바 표시, ViewModel에 결과 전달과 같은 일회성 이벤트를 처리할 때는 ResultEffect를 사용하세요.

ResultEffect는 수신 결과의 대기열을 유지합니다. 특정 키에 대해 여러 결과가 전송되면 ResultEffect는 전송된 순서대로 각 결과를 처리합니다. 또한 ResultEffect는 코루틴 범위에서 실행되므로 효과 본문 내에서 정지 함수를 직접 호출할 수 있습니다.

명시적 결과 키와 연결된 결과를 수신할 수 있습니다.

import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.result.ResultEffect

@Composable
fun RideSummaryScreen(
    onOpenAddressPicker: (key: String) -> Unit,
    viewModel: RideSummaryViewModel = viewModel()
) {
    ResultEffect<Address>(resultKey = "pickup_address") { address ->
        viewModel.onPickupAddressSelected(address)
    }

    ResultEffect<Address>(resultKey = "destination_address") { address ->
        viewModel.onDestinationAddressSelected(address)
    }

    RideSummaryContent(
        pickupAddress = viewModel.pickupAddress,
        destinationAddress = viewModel.destinationAddress,
        onPickPickup = { onOpenAddressPicker("pickup_address") },
        onPickDestination = { onOpenAddressPicker("destination_address") }
    )
}

타입에서 파생된 키를 사용하여 결과를 수신할 수도 있습니다.

import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.result.ResultEffect

@Composable
fun ComposeMessageScreen(
    onPickContact: () -> Unit,
    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
    viewModel: ComposeMessageViewModel = viewModel()
) {
    ResultEffect<Contact> { contact ->
        // Suspending calls are supported directly in the effect body
        snackbarHostState.showSnackbar("Selected ${contact.name}")
        viewModel.onRecipientSelected(contact)
    }

    ComposeMessageContent(
        recipient = viewModel.recipient,
        onPickContact = onPickContact
    )
}

대상 간에 이동할 때 ResultEffect는 다음 수명 주기 시퀀스를 통해 실행됩니다.

  1. 발신자가 내보냄: 발신자 대상이 resultBus.sendResult(resultKey = "pickup_address", address)를 사용하여 결과를 전송하고 백 스택을 팝합니다.
  2. 수신기에서 컴포지션 입력: 수신기 대상이 활성 화면이 되고 ResultEffect이 결과를 수신 대기하기 시작합니다.
  3. 수신기 결과 처리: ResultEffect는 해당 키에 전송된 각 결과의 효과 본문을 수신하고 실행하여 전송된 순서대로 모든 배출을 처리합니다.
  4. 수신자가 컴포지션에서 나감: 수신자 대상이 백 스택에서 팝되면 ResultEffect이 컴포지션에서 나가고 결과 수신을 중지합니다.

conflateAsState을 사용하여 최신 결과를 상태로 관찰

최신 결과 값만 있으면 로컬 UI 상태를 직접 수정하거나 필터링하고 결과가 업데이트될 때마다 Compose가 자동으로 리컴포즈되도록 하려면 ResultEventBus에서 conflateAsState를 호출하세요.

명시적 결과 키와 연결된 결과를 관찰할 수 있습니다.

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.navigation3.runtime.result.LocalResultEventBus

@Composable
fun ThemePreviewScreen(
    onOpenColorPicker: (key: String) -> Unit
) {
    val resultBus = LocalResultEventBus.current

    val primaryColor by resultBus.conflateAsState<Color>(
        resultKey = "primary_color",
        defaultValue = MaterialTheme.colorScheme.primary
    )

    val accentColor by resultBus.conflateAsState<Color>(
        resultKey = "accent_color",
        defaultValue = MaterialTheme.colorScheme.tertiary
    )

    ThemePreviewContent(
        primaryColor = primaryColor,
        accentColor = accentColor,
        onPickPrimary = { onOpenColorPicker("primary_color") },
        onPickAccent = { onOpenColorPicker("accent_color") }
    )
}

유형 파생 키를 사용하여 결과를 관찰할 수도 있습니다.

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.navigation3.runtime.result.LocalResultEventBus

@Composable
fun FilterableProductListScreen(
    initialFilter: ProductFilter = ProductFilter.All,
    onOpenFilterPicker: () -> Unit
) {
    val resultBus = LocalResultEventBus.current

    // Observe latest filter result as Compose State, starting with initialFilter
    val activeFilter by resultBus.conflateAsState<ProductFilter>(
        defaultValue = initialFilter
    )

    ProductListContent(
        activeFilter = activeFilter,
        onOpenFilterPicker = onOpenFilterPicker
    )
}

호이스트 ResultEventBus

기본적으로 rememberResultEventBusNavEntryDecoratorrememberResultEventBus를 사용하여 내부적으로 자체 ResultEventBus를 만들고 기억합니다.

다음과 같은 경우 ResultEventBus를 명시적으로 만들고 호이스팅할 수 있습니다.

  • ResultEventBus 인스턴스를 컴포저블이 아닌 구성요소나 종속 항목 삽입 그래프에 직접 전달합니다.
  • 대상 계층 구조 외부에서 최상위 앱 스캐폴딩 (예: 앱 바 또는 탐색 창)의 결과를 전송하거나 관찰합니다.

ResultEventBus를 호이스팅하려면 rememberResultEventBus를 사용하여 ResultEventBus를 만들고 rememberResultEventBusNavEntryDecorator(resultEventBus)에 전달합니다.

import androidx.compose.runtime.Composable
import androidx.navigation3.runtime.result.rememberResultEventBus
import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator
import androidx.navigation3.ui.NavDisplay

// Hoist the ResultEventBus at the top level
val resultEventBus = rememberResultEventBus()

// Pass the hoisted bus to the decorator
val resultEventBusNavEntryDecorator =
    rememberResultEventBusNavEntryDecorator<NavKey>(
        resultEventBus = resultEventBus
    )

NavDisplay(
    /* ... */
    entryDecorators = listOf(
        rememberSaveableStateHolderNavEntryDecorator(),
        resultEventBusNavEntryDecorator
    )
)

결과 관리 및 삭제

대상에서 일회성 결과를 사용하면 removeResult를 사용하여 이벤트 버스에서 결과를 삭제합니다. 이렇게 하면 대상이 컴포지션에 다시 진입할 때 버스가 이전 이벤트를 새 관찰자에게 다시 전달하지 않습니다.

import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.result.LocalResultEventBus
import androidx.navigation3.runtime.result.ResultEffect

@Composable
fun NotificationSettingsScreen(
    viewModel: NotificationViewModel = viewModel()
) {
    val resultBus = LocalResultEventBus.current

    ResultEffect<ConfirmationResult>(resultKey = "confirm_permission") { confirmation ->
        viewModel.onPermissionConfirmed(confirmation)

        // Clear the result after consumption to prevent re-delivery
        resultBus.removeResult(resultKey = "confirm_permission")
    }
}

명시적 키 (resultBus.removeResult(resultKey)) 또는 유형 파생 키 (resultBus.removeResult<T>())로 결과를 지울 수 있습니다. 키 일치에 관한 자세한 내용은 결과 키를 참고하세요.

레시피

다양한 결과 전달 전략을 보여주는 실행 가능한 전체 코드 예시는 다음 레시피를 참고하세요.