All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.slack.circuit.foundation.AnsweringNavigator.kt Maven / Gradle / Ivy

The newest version!
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.foundation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.slack.circuit.backstack.BackStack
import com.slack.circuit.runtime.GoToNavigator
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.screen.PopResult
import com.slack.circuit.runtime.screen.Screen
import kotlin.reflect.KClass
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import kotlinx.coroutines.CoroutineScope

/**
 * Returns whether or not answering navigation is available. This is essentially a proxy for whether
 * or not this composition is running within a [NavigableCircuitContent].
 */
@Composable public fun answeringNavigationAvailable(): Boolean = LocalBackStack.current != null

/**
 * A reified version of [rememberAnsweringNavigator]. See documented overloads of this function for
 * more information.
 */
@Composable
public inline fun  rememberAnsweringNavigator(
  fallbackNavigator: Navigator,
  noinline block: suspend CoroutineScope.(result: T) -> Unit,
): GoToNavigator = rememberAnsweringNavigator(fallbackNavigator, T::class, block)

/**
 * Returns a [GoToNavigator] that answers with the given [resultType] or defaults to
 * [fallbackNavigator] if no back stack is available to pass results through.
 */
@Composable
public fun  rememberAnsweringNavigator(
  fallbackNavigator: Navigator,
  resultType: KClass,
  block: suspend CoroutineScope.(result: T) -> Unit,
): GoToNavigator {
  val backStack = LocalBackStack.current ?: return fallbackNavigator
  return rememberAnsweringNavigator(backStack, resultType, block)
}

/**
 * A reified version of [rememberAnsweringNavigator]. See documented overloads of this function for
 * more information.
 */
@Composable
public inline fun  rememberAnsweringNavigator(
  backStack: BackStack,
  noinline block: suspend CoroutineScope.(result: T) -> Unit,
): GoToNavigator {
  return rememberAnsweringNavigator(backStack, T::class, block)
}

/**
 * Returns a [GoToNavigator] that answers with the given [resultType] using the given [backStack].
 *
 * Handling of the result type [T] should be handled in the [block] parameter and is guaranteed to
 * only be called _at most_ once. It may never be called if the navigated screen never pops with a
 * result (of equivalent type) back.
 *
 * Note that [resultType] is a simple instance check, so subtypes may also be valid answers.
 *
 * ## Example
 *
 * ```kotlin
 * val pickPhotoNavigator = rememberAnsweringNavigator(backStack, PickPhotoScreen.Result::class) { result: PickPhotoScreen.Result ->
 *   // Do something with the result!
 * }
 *
 * return State(...) { event ->
 *   when (event) {
 *     is PickPhoto -> pickPhotoNavigator.goTo(PickPhotoScreen)
 *   }
 * }
 *
 * // In PickPhotoScreen
 * navigator.pop(PickPhotoScreen.Result(...))
 * ```
 */
@Composable
public fun  rememberAnsweringNavigator(
  backStack: BackStack,
  resultType: KClass,
  block: suspend CoroutineScope.(result: T) -> Unit,
): GoToNavigator {
  val currentBackStack by rememberUpdatedState(backStack)
  val currentResultType by rememberUpdatedState(resultType)

  // Top screen at the start, so we can ensure we only collect the result if
  // we've returned to this screen
  val initialRecordKey = rememberSaveable {
    currentBackStack.topRecord?.key ?: error("Navigator must have a top screen at start.")
  }

  // Key for the resultKey, so we can track who owns this requested result
  val key = rememberSaveable { @OptIn(ExperimentalUuidApi::class) Uuid.random().toString() }

  // Current top record of the navigator
  val currentTopRecordKey by remember { derivedStateOf { currentBackStack.topRecord!!.key } }

  // Track whether we've actually gone to the next record yet
  var launched by rememberSaveable { mutableStateOf(false) }

  // Collect the result if we've launched and now returned to the initial record
  if (launched && currentTopRecordKey == initialRecordKey) {
    LaunchedEffect(key) {
      val result = currentBackStack.topRecord!!.awaitResult(key) ?: return@LaunchedEffect
      launched = false
      if (currentResultType.isInstance(result)) {
        @Suppress("UNCHECKED_CAST") block(result as T)
      }
    }
  }
  val answeringNavigator = remember {
    object : GoToNavigator {
      override fun goTo(screen: Screen): Boolean {
        currentBackStack.push(screen, key)
        launched = true
        return true
      }
    }
  }
  return answeringNavigator
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy