commonMain.ru.alexgladkov.odyssey.compose.RootController.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of odyssey-compose Show documentation
Show all versions of odyssey-compose Show documentation
Lightweight multiplatform navigation library (jvm, android, ios)
package ru.alexgladkov.odyssey.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import ru.alexgladkov.odyssey.compose.base.BottomBarNavigator
import ru.alexgladkov.odyssey.compose.base.Navigator
import ru.alexgladkov.odyssey.compose.base.TopBarNavigator
import ru.alexgladkov.odyssey.compose.controllers.ModalController
import ru.alexgladkov.odyssey.compose.controllers.MultiStackRootController
import ru.alexgladkov.odyssey.compose.controllers.TabNavigationModel
import ru.alexgladkov.odyssey.compose.extensions.createUniqueKey
import ru.alexgladkov.odyssey.compose.helpers.*
import ru.alexgladkov.odyssey.compose.local.LocalRootController
import ru.alexgladkov.odyssey.compose.navigation.bottom_bar_navigation.*
import ru.alexgladkov.odyssey.core.CoreRootController
import ru.alexgladkov.odyssey.core.LaunchFlag
import ru.alexgladkov.odyssey.core.NavConfiguration
import ru.alexgladkov.odyssey.core.animations.AnimationType
import ru.alexgladkov.odyssey.core.backpress.BackPressedCallback
import ru.alexgladkov.odyssey.core.backpress.OnBackPressedDispatcher
import ru.alexgladkov.odyssey.core.breadcrumbs.Breadcrumb
import ru.alexgladkov.odyssey.core.configuration.DisplayType
import ru.alexgladkov.odyssey.core.configuration.RootConfiguration
import ru.alexgladkov.odyssey.core.configuration.RootControllerType
import ru.alexgladkov.odyssey.core.screen.Screen
import ru.alexgladkov.odyssey.core.screen.ScreenBundle
import ru.alexgladkov.odyssey.core.wrap
import kotlin.collections.HashMap
typealias RenderWithParams = @Composable (T) -> Unit
typealias Render = @Composable (key: String) -> Unit
sealed class ScreenType {
object Simple : ScreenType()
data class Flow(val flowBuilderModel: FlowBuilderModel) : ScreenType()
data class MultiStack(
val multiStackBuilderModel: MultiStackBuilderModel,
val tabsNavModel: TabsNavModel
) : ScreenType()
}
data class AllowedDestination(
val key: String,
val screenType: ScreenType
)
open class RootController(
configuration: RootConfiguration = RootConfiguration(
rootControllerType = RootControllerType.Root,
displayType = DisplayType.FullScreen
)
) : CoreRootController(configuration = configuration) {
private val _allowedDestinations: MutableList = mutableListOf()
override val _backstack = mutableListOf()
private val _currentScreen: MutableStateFlow =
MutableStateFlow(null)
private var _childrenRootController: MutableList = mutableListOf()
private val _screenMap = mutableMapOf>()
private var _onBackPressedDispatcher: OnBackPressedDispatcher? = null
private var _modalController: ModalController? = null
private var _deepLinkUri: String? = null
override var onScreenNavigate: ((Breadcrumb) -> Unit)? = null
var parentRootController: RootController? = null
var backgroundColor: Color = Color.White
var onApplicationFinish: (() -> Unit)? = null
var onScreenRemove: (ScreenBundle) -> Unit =
{ parentRootController?.onScreenRemove?.invoke(it) }
var currentScreen: StateFlow = _currentScreen.asStateFlow()
/**
* Debug name need to debug :) if you like console debugging
* Setup automatically
*/
open var debugName: String? = if (parentRootController == null) "Root" else null
protected set
init {
initServiceScreens()
}
// Get screen render compose function
fun getScreenRender(screenName: String?): RenderWithParams? {
return when {
screenName?.contains(multiStackKey) == true -> _screenMap[multiStackKey]
screenName?.contains(flowKey) == true -> _screenMap[flowKey]
else -> _screenMap[screenName]
}
}
// Render screen with params
@Deprecated(
"Use renderScreen function instead",
ReplaceWith("renderScreen(screenName, params)")
)
@Composable
fun RenderScreen(screenName: String?, params: Any?) {
renderScreen(screenName, params)
}
@Composable
fun renderScreen(screenName: String?, params: Any?) {
_screenMap[screenName]?.invoke(params)
}
/**
* Update root controller screen map to find composables
* @param screenMap - generated screen map
*/
fun updateScreenMap(screenMap: HashMap>) {
_screenMap.putAll(screenMap)
}
/**
* Set allowed destinations to RootController
* @param list - list of destinations
* @see Destination to know more
*/
fun setNavigationGraph(list: List) {
_allowedDestinations.clear()
_allowedDestinations.addAll(list)
}
/**
* Call this to work with back press mechanism
* You can use it with android to handle hardware back press
* or can provide custom implementation
*/
fun setupBackPressedDispatcher(onBackPressedDispatcher: OnBackPressedDispatcher?) {
_onBackPressedDispatcher = onBackPressedDispatcher
_onBackPressedDispatcher?.backPressedCallback = object : BackPressedCallback() {
override fun onBackPressed() {
popBackStack()
}
}
}
/** Measure deep of root controller */
fun measureLevel(): Int = findRootController()._backstack.size
/**
* Send command to controller to launch new scenario
* Under the hood library check navigation graph and create simple screen,
* flow or multistack flow
* @param screen - screen code, for example "splash". Must be included
* in navigation graph or will cause an error
* @param startScreen - start screen for flow/multistack
* @param startTabPosition - start tab position for multistack
* @param params - any bunch of params you need for the screen
* @param animationType - preferred animationType
* @param launchFlag - flag if you want to change default behavior @see LaunchFlag
*/
fun launch(
screen: String,
startScreen: String? = null,
startTabPosition: Int = 0,
params: Any? = null,
animationType: AnimationType = AnimationType.None,
launchFlag: LaunchFlag? = null,
deepLink: Boolean = false
) {
if (deepLink && _deepLinkUri?.isNotBlank() == true) {
proceedDeepLink(animationType = animationType, launchFlag = launchFlag)
return
}
val screenType = _allowedDestinations.find { it.key == screen }?.screenType
?: throw IllegalStateException("Can't find screen in destination. Did you provide this screen?")
handleScreenBreadcrumbs(targetKey = screen)
when (screenType) {
is ScreenType.Flow -> launchFlowScreen(
screen,
startScreen,
params,
animationType,
screenType.flowBuilderModel,
launchFlag
)
is ScreenType.Simple -> launchSimpleScreen(screen, params, animationType, launchFlag)
is ScreenType.MultiStack<*> -> launchMultiStackScreen(
screenName = screen,
animationType = animationType,
multiStackBuilderModel = screenType.multiStackBuilderModel,
tabsNavModel = screenType.tabsNavModel,
launchFlag = launchFlag,
startScreen = startScreen,
startTabPosition = startTabPosition,
params = params
)
}
}
// Returns to previous screen
open fun popBackStack() {
if (_modalController?.isEmpty() == false) {
_modalController?.popBackStack()
return
}
val realKey = _backstack.last().realKey
when {
realKey.contains(flowKey) -> removeTopScreen(_childrenRootController.last())
realKey.contains(multiStackKey) -> _childrenRootController.last().popBackStack()
else -> removeTopScreen(this)
}
}
/**
* back to the first screen by name recursively
*/
fun backToScreen(screenName: String) {
_modalController?.clearBackStack()
val realKey = _backstack.last().realKey
when {
realKey.contains(flowKey) -> backToScreen(_childrenRootController.last(), screenName)
realKey.contains(multiStackKey) -> backToScreen(
_childrenRootController.last(),
screenName
)
else -> backToScreen(this, screenName)
}
}
//Find first RootController in hierarchy
fun findRootController(): RootController {
var currentRootController = this
while (currentRootController.parentRootController != null) {
currentRootController = currentRootController.parentRootController!!
}
return currentRootController
}
// Returns controller to show modal sheets
fun findModalController(): ModalController {
return if (_modalController == null) {
findRootController()._modalController!!
} else {
_modalController!!
}
}
// Returns your MultiStack host if it presented
fun findHostController(): MultiStackRootController? {
if (configuration.rootControllerType == RootControllerType.MultiStack) return this as? MultiStackRootController
if (configuration.rootControllerType == RootControllerType.Tab) {
return parentRootController as? MultiStackRootController
}
return null
}
/**
* Attaches Modal Controller to Root Controller
* @param modalController - controller to show modal sheets
*/
fun attachModalController(modalController: ModalController) {
this._modalController = modalController
}
/**
* Draws current screen in stack (need for Navigator)
* @param startScreen - draw start screen for flow/multistack
* @param startParams - param for startScreen in flow
*/
fun drawCurrentScreen(startScreen: String? = null, startParams: Any? = null) {
if (_backstack.isEmpty()) {
launch(
screen = _allowedDestinations.firstOrNull { it.key == startScreen }?.key
?: _allowedDestinations.first().key,
params = startParams
)
} else {
val current = _backstack.last()
_currentScreen.value = current.copy(animationType = AnimationType.None).wrap()
}
}
/**
* @param path - uri path
* Set information about deeplink
*/
fun setDeepLinkUri(path: String?) {
this._deepLinkUri = path
}
private fun proceedDeepLink(animationType: AnimationType, launchFlag: LaunchFlag?) {
val splitDeepLink = _deepLinkUri?.split("/") ?: emptyList()
if (splitDeepLink.size < 2) {
throw IllegalStateException("Deeplink $_deepLinkUri has illegal format")
}
val searchKey = splitDeepLink[1]
val params = if (splitDeepLink.size > 2) splitDeepLink[2] else null
var startTabPosition = 0
val destination = _allowedDestinations.firstOrNull { destination ->
when (val screen = destination.screenType) {
ScreenType.Simple -> searchKey == destination.key
is ScreenType.Flow -> screen.flowBuilderModel.allowedDestination.firstOrNull { it.key == searchKey } != null
is ScreenType.MultiStack<*> -> {
var containsScreen = false
run loop@{
screen.multiStackBuilderModel.tabItems.forEachIndexed { index, info ->
containsScreen =
info.allowedDestination.firstOrNull { it.key == searchKey } != null
if (containsScreen) {
startTabPosition = index
return@loop
}
}
}
containsScreen
}
}
} ?: throw IllegalStateException("Can't launch $_deepLinkUri to unknown screen")
launch(
screen = destination.key,
startScreen = searchKey,
startTabPosition = startTabPosition,
params = params,
animationType = animationType,
launchFlag = launchFlag,
deepLink = false
)
_deepLinkUri = null
}
private fun backToScreen(rootController: RootController?, screenName: String) {
rootController?.let {
val isLastScreen = it._backstack.size <= 1
if (isLastScreen) {
if (it.debugName == "Root") {
it.onApplicationFinish?.invoke()
} else {
val last = it._backstack.removeLast()
val parentController = it.parentRootController ?: return
val current = parentController._backstack.last()
val clearedKey = cleanRealKeyFromType(current.realKey)
if (clearedKey == screenName) {
parentController._currentScreen.value = current
.copy(animationType = last.animationType, isForward = false)
.wrap(with = last)
} else {
backToScreen(it.parentRootController, screenName)
}
}
} else {
val last = it._backstack.removeLast()
val current = it._backstack.last()
val clearedKey = cleanRealKeyFromType(current.realKey)
if (clearedKey == screenName) {
it._currentScreen.value = current
.copy(animationType = last.animationType, isForward = false)
.wrap(with = last)
} else {
backToScreen(rootController, screenName)
}
}
}
}
private fun removeTopScreen(rootController: RootController?) {
rootController?.let {
if (it._backstack.size <= 1) {
if (it.debugName == "Root") {
it.onApplicationFinish?.invoke()
} else {
removeTopScreen(it.parentRootController)
}
} else {
val last = it._backstack.removeLast()
val current = it._backstack.last()
it._currentScreen.value = current
.copy(animationType = last.animationType, isForward = false)
.wrap(with = last)
}
}
}
private fun launchSimpleScreen(
key: String,
params: Any?,
animationType: AnimationType,
launchFlag: LaunchFlag?
) {
val screen = Screen(
key = randomizeKey(key),
realKey = key,
params = params,
animationType = if (_backstack.isEmpty() && launchFlag == null) AnimationType.None else animationType
)
handleLaunchFlag(key, launchFlag)
_backstack.add(screen)
_currentScreen.value = screen.wrap()
}
private fun launchFlowScreen(
key: String,
startScreen: String?,
params: Any?,
animationType: AnimationType,
flowBuilderModel: FlowBuilderModel,
launchFlag: LaunchFlag?
) {
if (configuration.rootControllerType == RootControllerType.Flow) throw IllegalStateException(
"Don't use flow inside flow, call findRootController() instead"
)
val compositeKey = "$flowKey$$key"
handleLaunchFlag(compositeKey, launchFlag)
val rootController =
RootController(RootConfiguration(rootControllerType = RootControllerType.Flow))
rootController.backgroundColor = backgroundColor
rootController.debugName = key
rootController.parentRootController = this
rootController.onScreenNavigate = onScreenNavigate
rootController.onApplicationFinish = {
rootController.parentRootController?.popBackStack()
}
rootController.setDeepLinkUri(_deepLinkUri)
rootController.updateScreenMap(flowBuilderModel.screenMap)
rootController.setNavigationGraph(flowBuilderModel.allowedDestination)
_childrenRootController.add(rootController)
val targetScreen =
flowBuilderModel.allowedDestination.firstOrNull { startScreen == it.key }?.key
?: flowBuilderModel.allowedDestination.first().key
val screen = Screen(
key = randomizeKey(compositeKey),
realKey = compositeKey,
animationType = animationType,
params = FlowBundle(
key = targetScreen,
startScreen = targetScreen,
params = params,
rootController = rootController
)
)
_backstack.add(screen)
_currentScreen.value = screen.wrap()
}
private fun launchMultiStackScreen(
screenName: String,
animationType: AnimationType,
multiStackBuilderModel: MultiStackBuilderModel,
tabsNavModel: TabsNavModel<*>,
startScreen: String? = null,
startTabPosition: Int = 0,
launchFlag: LaunchFlag?,
params: Any? = null,
) {
if (configuration.rootControllerType == RootControllerType.Flow || configuration.rootControllerType == RootControllerType.MultiStack)
throw IllegalStateException("Don't use flow inside flow, call findRootController instead")
val multiStackRealKey = "$multiStackKey$$screenName"
handleLaunchFlag(multiStackRealKey, launchFlag)
val parentRootController = this
val multiStackRootController = MultiStackRootController(
rootControllerType = RootControllerType.MultiStack,
tabsNavModel = tabsNavModel,
)
multiStackRootController.setDeepLinkUri(_deepLinkUri)
multiStackRootController.parentRootController = parentRootController
multiStackRootController.onScreenNavigate = onScreenNavigate
val configurations = multiStackBuilderModel.tabItems.map {
val rootController =
RootController(RootConfiguration(rootControllerType = RootControllerType.Tab))
rootController.backgroundColor = backgroundColor
rootController.parentRootController = multiStackRootController
rootController.debugName = it.tabItem.name
rootController.onScreenNavigate = onScreenNavigate
rootController.setDeepLinkUri(_deepLinkUri)
rootController.updateScreenMap(it.screenMap)
rootController.setNavigationGraph(it.allowedDestination)
TabNavigationModel(tabInfo = it, rootController = rootController)
}
multiStackRootController.setupWithTabs(configurations, startTabPosition)
_childrenRootController.add(multiStackRootController)
val screen = Screen(
key = randomizeKey(multiStackRealKey),
realKey = multiStackRealKey,
animationType = animationType,
params = MultiStackBundle(
rootController = multiStackRootController,
startScreen = startScreen,
params = params
)
)
_backstack.add(screen)
_currentScreen.value = screen.wrap()
}
private fun initServiceScreens() {
if (configuration.rootControllerType == RootControllerType.Root) {
_screenMap[flowKey] = {
val bundle = it as FlowBundle
CompositionLocalProvider(
LocalRootController provides bundle.rootController
) {
Navigator(startScreen = bundle.startScreen, startParams = bundle.params)
}
}
_screenMap[multiStackKey] = {
val bundle = it as MultiStackBundle
CompositionLocalProvider(
LocalRootController provides bundle.rootController
) {
when (bundle.rootController.tabsNavModel.navConfiguration.type) {
TabsNavType.Bottom -> {
BottomBarNavigator(
startScreen = bundle.startScreen
)
}
TabsNavType.Top -> {
TopBarNavigator(
startScreen = bundle.startScreen
)
}
TabsNavType.Custom -> {
val customNavConfiguration =
bundle.rootController.tabsNavModel.navConfiguration as CustomNavConfiguration
customNavConfiguration.content(bundle.params)
}
}
}
}
}
}
private fun handleScreenBreadcrumbs(targetKey: String) {
val currentScreen = _backstack.lastOrNull()?.realKey ?: run {
onScreenNavigate?.invoke(
if (configuration.rootControllerType != RootControllerType.Tab) {
Breadcrumb.SimpleNavigation("Start", targetKey)
} else {
Breadcrumb.TabNavigation(debugName ?: "", "Start", targetKey)
}
)
return
}
onScreenNavigate?.invoke(
if (configuration.rootControllerType != RootControllerType.Tab) {
Breadcrumb.SimpleNavigation(cleanRealKeyFromType(currentScreen), targetKey)
} else {
Breadcrumb.TabNavigation(
debugName ?: "",
cleanRealKeyFromType(currentScreen),
targetKey
)
}
)
}
private fun handleLaunchFlag(screenKey: String, launchFlag: LaunchFlag?) {
when (launchFlag) {
LaunchFlag.SingleNewTask -> _backstack.clear()
LaunchFlag.SingleInstance -> _backstack.removeAll { it.realKey == screenKey }
LaunchFlag.ClearPrevious -> _backstack.removeLastOrNull()
null -> {}
}
}
companion object {
internal fun randomizeKey(key: String): String = createUniqueKey(key)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy