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

commonMain.moe.tlaster.precompose.navigation.BackStackManager.kt Maven / Gradle / Ivy

Go to download

A third-party Jetbrains Compose library with ViewModel, LiveData and Navigation support.

There is a newer version: 1.7.0-alpha03
Show newest version
package moe.tlaster.precompose.navigation

import androidx.compose.runtime.Stable
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.map
import moe.tlaster.precompose.lifecycle.Lifecycle
import moe.tlaster.precompose.lifecycle.LifecycleObserver
import moe.tlaster.precompose.lifecycle.LifecycleOwner
import moe.tlaster.precompose.navigation.route.SceneRoute
import moe.tlaster.precompose.navigation.route.isFloatingRoute
import moe.tlaster.precompose.navigation.route.isSceneRoute
import moe.tlaster.precompose.stateholder.SavedStateHolder
import moe.tlaster.precompose.stateholder.StateHolder
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.max

@Stable
internal class BackStackManager : LifecycleObserver {
    private lateinit var _stateHolder: StateHolder
    private lateinit var _savedStateHolder: SavedStateHolder

    // internal for testing
    internal val backStacks = MutableStateFlow(listOf())
    private var _routeParser = RouteParser()
    private val _suspendResult = linkedMapOf>()
    private var _routeGraph: RouteGraph? = null
        set(value) {
            field = value
            if (value != null) {
                _routeParser = RouteParser()
                value.routes
                    .map { route ->
                        RouteParser.expandOptionalVariables(route.route).let {
                            if (route is SceneRoute) {
                                it + route.deepLinks.flatMap {
                                    RouteParser.expandOptionalVariables(it)
                                }
                            } else {
                                it
                            }
                        } to route
                    }
                    .flatMap { it.first.map { route -> route to it.second } }
                    .forEach {
                        _routeParser.insert(it.first, it.second)
                    }
            }
        }
    val currentBackStackEntry: Flow
        get() = backStacks.asSharedFlow().map { it.lastOrNull() }

    val prevBackStackEntry: Flow
        get() = backStacks.asSharedFlow().map { it.dropLast(1).lastOrNull() }

    val canGoBack: Flow
        get() = backStacks.asSharedFlow().map { it.size > 1 }

    val currentSceneBackStackEntry: Flow
        get() = backStacks.asSharedFlow().map { it.lastOrNull { it.route.isSceneRoute() } }

    val prevSceneBackStackEntry: Flow
        get() = backStacks.asSharedFlow().map {
            it.dropLastWhile { !it.route.isSceneRoute() }
                .dropLast(1)
                .lastOrNull { it.route.isSceneRoute() }
        }

    val currentFloatingBackStackEntry: Flow
        get() = backStacks.asSharedFlow().map { it.lastOrNull { it.route.isFloatingRoute() } }

    fun init(
        stateHolder: StateHolder,
        savedStateHolder: SavedStateHolder,
        lifecycleOwner: LifecycleOwner,
    ) {
        _stateHolder = stateHolder
        _savedStateHolder = savedStateHolder
        lifecycleOwner.lifecycle.addObserver(this)
    }

    fun setRouteGraph(
        routeGraph: RouteGraph,
    ) {
        if (_routeGraph != routeGraph) {
            _routeGraph?.let {
                // clear all backstacks
                backStacks.value.forEach {
                    it.destroy()
                }
                backStacks.value = emptyList()
            }
            _routeGraph = routeGraph
            // push to initial route
            push(routeGraph.initialRoute)
        } else {
            _routeGraph = routeGraph
            // update routes
            backStacks.value.forEach { entry ->
                entry.routeInternal = _routeParser.find(entry.path)?.route ?: entry.routeInternal
            }
        }
    }

    fun push(path: String, options: NavOptions? = null) {
        val currentBackStacks = backStacks.value
        val query = path.substringAfter('?', "")
        val routePath = path.substringBefore('?')
        val matchResult = _routeParser.find(path = routePath)
        checkNotNull(matchResult) { "RouteStackManager: navigate target $path not found" }
        // require(matchResult.route is ComposeRoute) { "RouteStackManager: navigate target $path is not ComposeRoute" }
        if ( // for launchSingleTop
            options != null &&
            options.launchSingleTop &&
            currentBackStacks.any { it.hasRoute(matchResult.route.route, path, options.includePath) }
        ) {
            currentBackStacks.firstOrNull { it.hasRoute(matchResult.route.route, path, options.includePath) }
                ?.let { entry ->
                    backStacks.value = backStacks.value.filter { it.stateId != entry.stateId } + entry
                }
        } else {
            backStacks.value += BackStackEntry(
                stateId = uuid4().toString(),
                routeInternal = matchResult.route,
                pathMap = matchResult.pathMap,
                queryString = query.takeIf { it.isNotEmpty() }?.let {
                    QueryString(it)
                },
                path = path,
                parentStateHolder = _stateHolder,
                parentSavedStateHolder = _savedStateHolder,
            )
        }

        if (options != null && options.popUpTo != PopUpTo.None) {
            val backStack = if (options.launchSingleTop) {
                backStacks.value.dropLast(1)
            } else {
                currentBackStacks
            }

            val popUpTo = options.popUpTo
            val index = when (popUpTo) {
                PopUpTo.None -> -1
                PopUpTo.Prev -> backStack.lastIndex - 1
                is PopUpTo.Route -> if (popUpTo.route.isNotEmpty()) {
                    backStack.indexOfLast { it.hasRoute(popUpTo.route, path, options.includePath) }
                } else {
                    0
                }
            }
            if (index != -1) {
                val stacksToDrop = backStack.subList(
                    if (popUpTo.inclusive) index else index + 1,
                    backStack.size,
                )
                backStacks.value -= stacksToDrop
                stacksToDrop.forEach {
                    it.destroy()
                }
            }
        }
    }

    fun pop(result: Any? = null) {
        val currentBackStacks = backStacks.value
        if (currentBackStacks.size > 1) {
            val last = currentBackStacks.last()
            backStacks.value = currentBackStacks.dropLast(1)
            last.destroy()
            _suspendResult.remove(last)?.resume(result)
        }
    }

    fun popWithOptions(
        popUpTo: PopUpTo,
    ) {
        val currentBackStacks = backStacks.value
        if (currentBackStacks.size <= 1) {
            return
        }
        val index = when (popUpTo) {
            PopUpTo.None -> -1
            PopUpTo.Prev -> currentBackStacks.lastIndex - 1
            is PopUpTo.Route -> if (popUpTo.route.isNotEmpty()) {
                currentBackStacks.indexOfLast { it.hasRoute(popUpTo.route, "", false) }
            } else {
                0
            }
        }.let {
            if (popUpTo.inclusive) it else it + 1
        }.let {
            max(it, 0)
        }
        if (index != -1) {
            val stacksToDrop = currentBackStacks.subList(
                index,
                currentBackStacks.size,
            )
            backStacks.value -= stacksToDrop
            stacksToDrop.forEach {
                _suspendResult.remove(it)?.resume(null)
                it.destroy()
            }
        }
    }

    suspend fun pushForResult(path: String, options: NavOptions? = null): Any? {
        return suspendCoroutine { continuation ->
            push(path, options)
            _suspendResult[backStacks.value.last()] = continuation
        }
    }

    override fun onStateChanged(state: Lifecycle.State) {
        when (state) {
            Lifecycle.State.Initialized -> Unit
            Lifecycle.State.Active -> {
                val currentEntry = backStacks.value.lastOrNull()
                currentEntry?.active()
            }

            Lifecycle.State.InActive -> {
                val currentEntry = backStacks.value.lastOrNull()
                currentEntry?.inActive()
            }

            Lifecycle.State.Destroyed -> {
                backStacks.value.forEach {
                    it.destroy()
                }
                backStacks.value = emptyList()
            }
        }
    }

    fun contains(entry: BackStackEntry): Boolean {
        return backStacks.value.contains(entry)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy