blob: a6e420db534bfa96dc64148033b58a895e934f97 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.navigation
import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.annotation.RestrictTo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* The NavigatorState encapsulates the state shared between the [Navigator] and the
* [NavController].
*/
public abstract class NavigatorState {
private val backStackLock = ReentrantLock(true)
private val _backStack: MutableStateFlow<List<NavBackStackEntry>> = MutableStateFlow(listOf())
private val _transitionsInProgress: MutableStateFlow<Set<NavBackStackEntry>> =
MutableStateFlow(setOf())
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public var isNavigating = false
/**
* While the [NavController] is responsible for the combined back stack across all
* Navigators, this back stack is specifically the set of destinations associated
* with this Navigator.
*
* Changing the back stack must be done via [push] and [pop].
*/
public val backStack: StateFlow<List<NavBackStackEntry>> = _backStack.asStateFlow()
/**
* This is the set of currently running transitions. Use this set to retrieve the entry and call
* [markTransitionComplete] once the transition is complete.
*/
public val transitionsInProgress: StateFlow<Set<NavBackStackEntry>> =
_transitionsInProgress.asStateFlow()
/**
* Adds the given [backStackEntry] to the [backStack].
*/
public open fun push(backStackEntry: NavBackStackEntry) {
backStackLock.withLock {
_backStack.value = _backStack.value + backStackEntry
}
}
/**
* Adds the given [backStackEntry] to the [backStack]. This also adds the given and
* previous entry to the [set of in progress transitions][transitionsInProgress].
* Added entries have their [Lifecycle] capped at [Lifecycle.State.STARTED] until an entry is
* passed into the [markTransitionComplete] callback, when they are allowed to go to
* [Lifecycle.State.RESUMED].
*
* @see transitionsInProgress
* @see markTransitionComplete
* @see popWithTransition
*/
public open fun pushWithTransition(backStackEntry: NavBackStackEntry) {
val previousEntry = backStack.value.lastOrNull()
// When navigating, we need to mark the outgoing entry as transitioning until it
// finishes its outgoing animation.
if (previousEntry != null) {
_transitionsInProgress.value = _transitionsInProgress.value + previousEntry
}
_transitionsInProgress.value = _transitionsInProgress.value + backStackEntry
push(backStackEntry)
}
/**
* Create a new [NavBackStackEntry] from a given [destination] and [arguments].
*/
public abstract fun createBackStackEntry(
destination: NavDestination,
arguments: Bundle?
): NavBackStackEntry
/**
* Pop all destinations up to and including [popUpTo]. This will remove those
* destinations from the [backStack], saving their state if [saveState] is `true`.
*/
public open fun pop(popUpTo: NavBackStackEntry, saveState: Boolean) {
backStackLock.withLock {
_backStack.value = _backStack.value.takeWhile { it != popUpTo }
}
}
/**
* Pops all destinations up to and including [popUpTo]. This also adds the given and
* incoming entry to the [set of in progress transitions][transitionsInProgress]. Added
* entries have their [Lifecycle] held at [Lifecycle.State.CREATED] until an entry is
* passed into the [markTransitionComplete] callback, when they are allowed to go to
* [Lifecycle.State.DESTROYED] and have their state cleared.
*
* This will remove those destinations from the [backStack], saving their state if
* [saveState] is `true`.
*
* @see transitionsInProgress
* @see markTransitionComplete
* @see pushWithTransition
*/
public open fun popWithTransition(popUpTo: NavBackStackEntry, saveState: Boolean) {
_transitionsInProgress.value = _transitionsInProgress.value + popUpTo
val incomingEntry = backStack.value.lastOrNull { entry ->
entry != popUpTo &&
backStack.value.lastIndexOf(entry) < backStack.value.lastIndexOf(popUpTo)
}
// When popping, we need to mark the incoming entry as transitioning so we keep it
// STARTED until the transition completes at which point we can move it to RESUMED
if (incomingEntry != null) {
_transitionsInProgress.value = _transitionsInProgress.value + incomingEntry
}
pop(popUpTo, saveState)
}
/**
* Informational callback indicating that the given [backStackEntry] has been
* affected by a [NavOptions.shouldLaunchSingleTop] operation.
*/
@CallSuper
public open fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) {
// We update the back stack here because we don't want to leave it to the navigator since
// it might be using transitions.
_backStack.value = _backStack.value - _backStack.value.last() + backStackEntry
}
/**
* This removes the given [NavBackStackEntry] from the [set of the transitions in
* progress][transitionsInProgress]. This should be called in conjunction with
* [pushWithTransition] and [popWithTransition] as those call are responsible for adding
* entries to [transitionsInProgress].
*
* Failing to call this method could result in entries being prevented from reaching their
* final [Lifecycle.State]}.
*
* @see pushWithTransition
* @see popWithTransition
*/
public open fun markTransitionComplete(entry: NavBackStackEntry) {
_transitionsInProgress.value = _transitionsInProgress.value - entry
}
}