Created
May 19, 2025 23:31
-
-
Save CXwudi/03c5c7fae17518912b305f4e4b0423ea to your computer and use it in GitHub Desktop.
Decompose ChildPanels with Customizable Back Handling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:OptIn(ExperimentalDecomposeApi::class) | |
package mikufan.cx.conduit.frontend.logic.component.custom | |
import com.arkivanov.decompose.ExperimentalDecomposeApi | |
import com.arkivanov.decompose.router.panels.ChildPanelsMode | |
import com.arkivanov.decompose.router.panels.Panels | |
/** | |
* A functional interface for handling back button presses in child panels. | |
* The implementation should determine what action to take when back button is pressed | |
* based on the current state of the panels. | |
* | |
* @param MC Main component configuration type | |
* @param DC Details component configuration type | |
* @param EC Extra component configuration type | |
*/ | |
@OptIn(ExperimentalDecomposeApi::class) | |
fun interface ChildPanelsBackHandler<MC : Any, DC : Any, EC : Any> { | |
/** | |
* Handle back button press for the given panels state. | |
* | |
* @param panels Current panels state | |
* @return The callback of the modified panels state after handling the back button press, or null if | |
* the back button press should be ignored or handled by parent components. | |
*/ | |
fun handle(panels: Panels<MC, DC, EC>): (() -> Panels<MC, DC, EC>)? | |
} | |
/** | |
* Creates a back handler that only handles back navigation in SINGLE mode. | |
* This is the default handler in standard Decompose. | |
* | |
* The handler closes details panel if open, or extra panel if open. | |
* Does nothing in DUAL or TRIPLE modes. | |
* | |
* @return A [ChildPanelsBackHandler] function object | |
*/ | |
@OptIn(ExperimentalDecomposeApi::class) | |
class SingleModeChildPanelsBackHandler<MC : Any, DC : Any, EC : Any> : ChildPanelsBackHandler<MC, DC, EC> { | |
override fun handle(panels: Panels<MC, DC, EC>): (() -> Panels<MC, DC, EC>)? = when { | |
(panels.mode == ChildPanelsMode.SINGLE) && (panels.extra != null) -> { | |
{ panels.copy(extra = null) } | |
} | |
(panels.mode == ChildPanelsMode.SINGLE) && (panels.details != null) -> { | |
{ panels.copy(details = null) } | |
} | |
else -> null | |
} | |
} | |
/** | |
* Creates a back handler that handles back navigation in all panel modes. | |
* | |
* In SINGLE mode, it behaves like [SingleModeChildPanelsBackHandler]. | |
* In TRIPLE mode, pressing back will close the extra panel and switch to DUAL mode. | |
* In DUAL mode, pressing back will close the details panel and switch to SINGLE mode. | |
* | |
* @return A [ChildPanelsBackHandler] function object | |
*/ | |
@OptIn(ExperimentalDecomposeApi::class) | |
class MultiModeChildPanelsBackHandler<MC : Any, DC : Any, EC : Any> : ChildPanelsBackHandler<MC, DC, EC> { | |
override fun handle(panels: Panels<MC, DC, EC>): (() -> Panels<MC, DC, EC>)? = | |
when { | |
(panels.mode == ChildPanelsMode.SINGLE) && (panels.extra != null) -> { | |
{ panels.copy(extra = null) } | |
} | |
(panels.mode == ChildPanelsMode.SINGLE) && (panels.details != null) -> { | |
{ panels.copy(details = null) } | |
} | |
// Added two more customizations for back button handling, where in the wide screen, going back will close the right-most panel | |
(panels.mode == ChildPanelsMode.TRIPLE) && (panels.extra != null) -> { | |
{ panels.copy(extra = null, mode = ChildPanelsMode.DUAL) } | |
} | |
(panels.mode == ChildPanelsMode.DUAL) && (panels.details != null) -> { | |
{ panels.copy(details = null, mode = ChildPanelsMode.SINGLE) } | |
} | |
// no handling, let the other back handler do its job | |
else -> null | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:OptIn(ExperimentalDecomposeApi::class) | |
package mikufan.cx.conduit.frontend.logic.component.custom | |
import com.arkivanov.decompose.Child | |
import com.arkivanov.decompose.ExperimentalDecomposeApi | |
import com.arkivanov.decompose.GenericComponentContext | |
import com.arkivanov.decompose.router.children.ChildNavState | |
import com.arkivanov.decompose.router.children.ChildNavState.Status | |
import com.arkivanov.decompose.router.children.NavState | |
import com.arkivanov.decompose.router.children.NavigationSource | |
import com.arkivanov.decompose.router.children.SimpleChildNavState | |
import com.arkivanov.decompose.router.children.children | |
import com.arkivanov.decompose.router.panels.ChildPanels | |
import com.arkivanov.decompose.router.panels.ChildPanelsMode | |
import com.arkivanov.decompose.router.panels.Panels | |
import com.arkivanov.decompose.router.panels.PanelsNavigation.Event | |
import com.arkivanov.decompose.value.Value | |
import com.arkivanov.essenty.statekeeper.SerializableContainer | |
import kotlinx.serialization.ExperimentalSerializationApi | |
import kotlinx.serialization.KSerializer | |
import kotlinx.serialization.builtins.NothingSerializer | |
/** | |
* This is a modified version of the original ChildPanels from the Decompose library | |
* at [ChildPanelsFactory.kt](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/ChildPanelsFactory.kt). | |
* This version enhances back-button handling: it’s now customizable. Defaulting to [SingleModeChildPanelsBackHandler], can also use [MultiModeChildPanelsBackHandler] or provide your own implementation. | |
* | |
* Initializes and manages a set of up to two child components (panels): Main (required) and | |
* Details (optional). The Extra component is unused. See [ChildPanelsMode] for documentation about | |
* how child components lifecycles are controlled. | |
* | |
* **It is strongly recommended to call this method on the Main thread.** | |
* | |
* @param source a source of navigation events. | |
* @param serializers an optional [Pair] of [KSerializer] (Main and Details) to be used for | |
* serializing and deserializing configurations. If `null` then the navigation state will not be preserved. | |
* @param initialPanels an initial state of Child Panels that should be set if there is no saved state. | |
* See [Panels] for more information. | |
* @param key a key of the navigation, must be unique if there are multiple Child Panels | |
* used in the same component. | |
* @param onStateChanged called every time the navigation state changes, `oldState` is `null` when | |
* called first time during initialisation. | |
* @param handleBackButton determines whether the backHandler should be registered. | |
* @param backHandler a custom back handler that determines how to handle back button presses. | |
* By default, uses [SingleModeChildPanelsBackHandler], | |
* matching the original Decompose library behavior, but can be overridden. | |
* This is only used if handleBackButton is true. | |
* @param mainFactory a factory function that creates new instances of the Main component. | |
* @param detailsFactory a factory function that creates new instances of the Details component. | |
* @return an observable [Value] of [ChildPanels]. | |
*/ | |
@ExperimentalSerializationApi | |
@ExperimentalDecomposeApi | |
fun <Ctx : GenericComponentContext<Ctx>, MC : Any, MT : Any, DC : Any, DT : Any> Ctx.customizableBackHandlerChildPanels( | |
source: NavigationSource<Event<MC, DC, Nothing>>, | |
serializers: Pair<KSerializer<MC>, KSerializer<DC>>?, | |
initialPanels: () -> Panels<MC, DC, Nothing>, | |
key: String = "DefaultChildPanels", | |
onStateChanged: (newState: Panels<MC, DC, Nothing>, oldState: Panels<MC, DC, Nothing>?) -> Unit = { _, _ -> }, | |
handleBackButton: Boolean = false, | |
backHandler: ChildPanelsBackHandler<MC, DC, Nothing> = SingleModeChildPanelsBackHandler(), | |
mainFactory: (configuration: MC, Ctx) -> MT, | |
detailsFactory: (configuration: DC, Ctx) -> DT, | |
): Value<ChildPanels<MC, MT, DC, DT, Nothing, Nothing>> = | |
customizableBackHandlerChildPanels( | |
source = source, | |
initialPanels = initialPanels, | |
serializers = serializers?.let { Triple(it.first, it.second, NothingSerializer()) }, | |
key = key, | |
onStateChanged = onStateChanged, | |
handleBackButton = handleBackButton, | |
backHandler = backHandler, | |
mainFactory = mainFactory, | |
detailsFactory = detailsFactory, | |
extraFactory = { _, _ -> error("Can't instantiate Nothing") }, | |
) | |
/** | |
* This is a modified version of the original ChildPanels from the Decompose library | |
* at [ChildPanelsFactory.kt](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/ChildPanelsFactory.kt). | |
* This version enhances back-button handling: it’s now customizable. Defaulting to [SingleModeChildPanelsBackHandler], can also use [MultiModeChildPanelsBackHandler] or provide your own implementation. | |
* | |
* Initializes and manages a set of up to two child components (panels): Main (required) and | |
* Details (optional). The Extra component is unused. See [ChildPanelsMode] for documentation about | |
* how child components lifecycles are controlled. | |
* | |
* **It is strongly recommended to call this method on the Main thread.** | |
* | |
* @param source a source of navigation events. | |
* @param initialPanels an initial state of Child Panels that should be set if there is no saved state. | |
* See [Panels] for more information. | |
* @param savePanels a function that saves the provided [Panels] state into [SerializableContainer]. | |
* The navigation state is not saved if `null` is returned. | |
* @param restorePanels a function that restores the [Panels] state from the provided [SerializableContainer]. | |
* If `null` is returned then [initialPanels] is used instead. | |
* The restored [Panels] state must have exactly the same configurations. | |
* @param key a key of the navigation, must be unique if there are multiple Child Panels | |
* used in the same component. | |
* @param onStateChanged called every time the navigation state changes, `oldState` is `null` when | |
* called first time during initialisation. | |
* @param handleBackButton determines whether the backHandler should be registered. | |
* @param backHandler a custom back handler that determines how to handle back button presses. | |
* By default, uses [SingleModeChildPanelsBackHandler], | |
* matching the original Decompose library behavior, but can be overridden. | |
* @param mainFactory a factory function that creates new instances of the Main component. | |
* @param detailsFactory a factory function that creates new instances of the Details component. | |
* @return an observable [Value] of [ChildPanels]. | |
*/ | |
@ExperimentalDecomposeApi | |
fun <Ctx : GenericComponentContext<Ctx>, MC : Any, MT : Any, DC : Any, DT : Any, EC : Any, ET : Any> Ctx.customizableBackHandlerChildPanels( | |
source: NavigationSource<Event<MC, DC, EC>>, | |
initialPanels: () -> Panels<MC, DC, EC>, | |
savePanels: (Panels<MC, DC, EC>) -> SerializableContainer?, | |
restorePanels: (SerializableContainer) -> Panels<MC, DC, EC>?, | |
key: String = "DefaultChildPanels", | |
onStateChanged: (newState: Panels<MC, DC, EC>, oldState: Panels<MC, DC, EC>?) -> Unit = { _, _ -> }, | |
handleBackButton: Boolean = false, | |
backHandler: ChildPanelsBackHandler<MC, DC, EC> = SingleModeChildPanelsBackHandler(), | |
mainFactory: (configuration: MC, Ctx) -> MT, | |
detailsFactory: (configuration: DC, Ctx) -> DT, | |
): Value<ChildPanels<MC, MT, DC, DT, EC, ET>> = | |
customizableBackHandlerChildPanels( | |
source = source, | |
initialPanels = initialPanels, | |
savePanels = savePanels, | |
restorePanels = restorePanels, | |
key = key, | |
onStateChanged = onStateChanged, | |
handleBackButton = handleBackButton, | |
backHandler = backHandler, | |
mainFactory = mainFactory, | |
detailsFactory = detailsFactory, | |
extraFactory = { _, _ -> error("Can't instantiate Nothing") }, | |
) | |
/** | |
* This is a modified version of the original ChildPanels from the Decompose library | |
* at [ChildPanelsFactory.kt](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/ChildPanelsFactory.kt). | |
* This version enhances back-button handling: it’s now customizable. Defaulting to [SingleModeChildPanelsBackHandler], can also use [MultiModeChildPanelsBackHandler] or provide your own implementation. | |
* | |
* Initializes and manages a set of up to three child components (panels): Main (required), | |
* Details (optional) and Extra (optional). See [ChildPanelsMode] for documentation about | |
* how child components lifecycles are controlled. | |
* | |
* **It is strongly recommended to call this method on the Main thread.** | |
* | |
* @param source a source of navigation events. | |
* @param serializers an optional [Triple] of [KSerializer] (Main, Details and Extra) to be used for | |
* serializing and deserializing configurations. If `null` then the navigation state will not be preserved. | |
* @param initialPanels an initial state of Child Panels that should be set if there is no saved state. | |
* See [Panels] for more information. | |
* @param key a key of the navigation, must be unique if there are multiple Child Panels | |
* used in the same component. | |
* @param onStateChanged called every time the navigation state changes, `oldState` is `null` when | |
* called first time during initialisation. | |
* @param handleBackButton determines whether the backHandler should be registered. | |
* @param backHandler a custom back handler that determines how to handle back button presses. | |
* By default, uses [SingleModeChildPanelsBackHandler], | |
* matching the original Decompose library behavior, but can be overridden. | |
* @param mainFactory a factory function that creates new instances of the Main component. | |
* @param detailsFactory a factory function that creates new instances of the Details component. | |
* @param extraFactory a factory function that creates new instances of the Extra component. | |
* @return an observable [Value] of [ChildPanels]. | |
*/ | |
@ExperimentalDecomposeApi | |
fun <Ctx : GenericComponentContext<Ctx>, MC : Any, MT : Any, DC : Any, DT : Any, EC : Any, ET : Any> Ctx.customizableBackHandlerChildPanels( | |
source: NavigationSource<Event<MC, DC, EC>>, | |
serializers: Triple<KSerializer<MC>, KSerializer<DC>, KSerializer<EC>>?, | |
initialPanels: () -> Panels<MC, DC, EC>, | |
key: String = "DefaultChildPanels", | |
onStateChanged: (newState: Panels<MC, DC, EC>, oldState: Panels<MC, DC, EC>?) -> Unit = { _, _ -> }, | |
handleBackButton: Boolean = false, | |
backHandler: ChildPanelsBackHandler<MC, DC, EC> = SingleModeChildPanelsBackHandler(), | |
mainFactory: (configuration: MC, Ctx) -> MT, | |
detailsFactory: (configuration: DC, Ctx) -> DT, | |
extraFactory: (configuration: EC, Ctx) -> ET, | |
): Value<ChildPanels<MC, MT, DC, DT, EC, ET>> = | |
customizableBackHandlerChildPanels( | |
source = source, | |
initialPanels = initialPanels, | |
savePanels = savePanels@{ panels -> | |
val (mainSerializer, detailsSerializer, extraSerializer) = serializers | |
?: return@savePanels null | |
SerializableContainer( | |
value = panels, | |
strategy = Panels.serializer(mainSerializer, detailsSerializer, extraSerializer), | |
) | |
}, | |
restorePanels = restorePanels@{ container -> | |
val (mainSerializer, detailsSerializer, extraSerializer) = serializers | |
?: return@restorePanels null | |
container.consume(Panels.serializer(mainSerializer, detailsSerializer, extraSerializer)) | |
}, | |
key = key, | |
onStateChanged = onStateChanged, | |
handleBackButton = handleBackButton, | |
backHandler = backHandler, | |
mainFactory = mainFactory, | |
detailsFactory = detailsFactory, | |
extraFactory = extraFactory, | |
) | |
/** | |
* This is a modified version of the original ChildPanels from the Decompose library | |
* at [ChildPanelsFactory.kt](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/ChildPanelsFactory.kt). | |
* This version enhances back-button handling: it’s now customizable. Defaulting to [SingleModeChildPanelsBackHandler], can also use [MultiModeChildPanelsBackHandler] or provide your own implementation. | |
* | |
* Initializes and manages a set of up to three child components (panels): Main (required), | |
* Details (optional) and Extra (optional). See [ChildPanelsMode] for documentation about | |
* how child components lifecycles are controlled. | |
* | |
* **It is strongly recommended to call this method on the Main thread.** | |
* | |
* @param source a source of navigation events. | |
* @param initialPanels an initial state of Child Panels that should be set if there is no saved state. | |
* See [Panels] for more information. | |
* @param savePanels a function that saves the provided [Panels] state into [SerializableContainer]. | |
* The navigation state is not saved if `null` is returned. | |
* @param restorePanels a function that restores the [Panels] state from the provided [SerializableContainer]. | |
* If `null` is returned then [initialPanels] is used instead. | |
* The restored [Panels] state must have exactly the same configurations. | |
* @param key a key of the navigation, must be unique if there are multiple Child Panels | |
* used in the same component. | |
* @param onStateChanged called every time the navigation state changes, `oldState` is `null` when | |
* called first time during initialisation. | |
* @param handleBackButton determines whether the backHandler should be registered. | |
* @param backHandler a custom back handler that determines how to handle back button presses. | |
* By default, uses [SingleModeChildPanelsBackHandler], | |
* matching the original Decompose library behavior, but can be overridden. | |
* @param mainFactory a factory function that creates new instances of the Main component. | |
* @param detailsFactory a factory function that creates new instances of the Details component. | |
* @param extraFactory a factory function that creates new instances of the Extra component. | |
* @return an observable [Value] of [ChildPanels]. | |
*/ | |
@ExperimentalDecomposeApi | |
fun <Ctx : GenericComponentContext<Ctx>, MC : Any, MT : Any, DC : Any, DT : Any, EC : Any, ET : Any> Ctx.customizableBackHandlerChildPanels( | |
source: NavigationSource<Event<MC, DC, EC>>, | |
initialPanels: () -> Panels<MC, DC, EC>, | |
savePanels: (Panels<MC, DC, EC>) -> SerializableContainer?, | |
restorePanels: (SerializableContainer) -> Panels<MC, DC, EC>?, | |
key: String = "DefaultChildPanels", | |
onStateChanged: (newState: Panels<MC, DC, EC>, oldState: Panels<MC, DC, EC>?) -> Unit = { _, _ -> }, | |
handleBackButton: Boolean = false, | |
backHandler: ChildPanelsBackHandler<MC, DC, EC> = SingleModeChildPanelsBackHandler(), | |
mainFactory: (configuration: MC, Ctx) -> MT, | |
detailsFactory: (configuration: DC, Ctx) -> DT, | |
extraFactory: (configuration: EC, Ctx) -> ET, | |
): Value<ChildPanels<MC, MT, DC, DT, EC, ET>> = | |
children( | |
source = source, | |
key = key, | |
initialState = { PanelsNavState(initialPanels()) }, | |
saveState = { savePanels(it.panels) }, | |
restoreState = { restorePanels(it)?.let(::PanelsNavState) }, | |
navTransformer = { state, event -> PanelsNavState(event.transformer(state.panels)) }, | |
stateMapper = { state, children -> | |
val main = children.firstNotNullOf { it.instance as? Panel.Main } | |
val details = children.firstNotNullOfOrNull { it.instance as? Panel.Details } | |
val extra = children.firstNotNullOfOrNull { it.instance as? Panel.Extra } | |
ChildPanels( | |
main = Child.Created(configuration = main.config, instance = main.instance), | |
details = details?.let { Child.Created(configuration = it.config, instance = it.instance) }, | |
extra = extra?.let { Child.Created(configuration = it.config, instance = it.instance) }, | |
mode = state.panels.mode, | |
) | |
}, | |
onStateChanged = { newState, oldState -> onStateChanged(newState.panels, oldState?.panels) }, | |
onEventComplete = { event, newState, oldState -> | |
event.onComplete( | |
newState.panels, | |
oldState.panels | |
) | |
}, | |
backTransformer = { state -> | |
if (!handleBackButton) return@children null | |
val newPanelHandler: (() -> Panels<MC, DC, EC>)? = backHandler.handle(state.panels) | |
newPanelHandler?.let { handler -> { state.copy(panels = handler()) } } | |
}, | |
childFactory = { config, ctx -> | |
when (config) { | |
is Config.Main -> Panel.Main(config.config, mainFactory(config.config, ctx)) | |
is Config.Details -> Panel.Details(config.config, detailsFactory(config.config, ctx)) | |
is Config.Extra -> Panel.Extra(config.config, extraFactory(config.config, ctx)) | |
} | |
}, | |
) | |
private sealed interface Config<out MC : Any, out DC : Any, out EC : Any> { | |
data class Main<out MC : Any>(val config: MC) : Config<MC, Nothing, Nothing> | |
data class Details<out DC : Any>(val config: DC) : Config<Nothing, DC, Nothing> | |
data class Extra<out EC : Any>(val config: EC) : Config<Nothing, Nothing, EC> | |
} | |
private sealed interface Panel<out MC : Any, out MT : Any, out DC : Any, out DT : Any, out EC : Any, out ET : Any> { | |
data class Main<out MC : Any, out MT : Any>(val config: MC, val instance: MT) : | |
Panel<MC, MT, Nothing, Nothing, Nothing, Nothing> | |
data class Details<out DC : Any, out DT : Any>(val config: DC, val instance: DT) : | |
Panel<Nothing, Nothing, DC, DT, Nothing, Nothing> | |
data class Extra<out EC : Any, out ET : Any>(val config: EC, val instance: ET) : | |
Panel<Nothing, Nothing, Nothing, Nothing, EC, ET> | |
} | |
private data class PanelsNavState<out MC : Any, out DC : Any, out EC : Any>( | |
val panels: Panels<MC, DC, EC>, | |
) : NavState<Config<MC, DC, EC>> { | |
override val children: List<ChildNavState<Config<MC, DC, EC>>> = | |
listOfNotNull( | |
SimpleChildNavState( | |
configuration = Config.Main(panels.main), | |
status = when { | |
panels.mode != ChildPanelsMode.SINGLE -> Status.RESUMED | |
(panels.details == null) && (panels.extra == null) -> Status.RESUMED | |
else -> Status.CREATED | |
}, | |
), | |
panels.details?.let { | |
SimpleChildNavState( | |
configuration = Config.Details(it), | |
status = when { | |
panels.mode == ChildPanelsMode.TRIPLE -> Status.RESUMED | |
panels.extra == null -> Status.RESUMED | |
else -> Status.CREATED | |
}, | |
) | |
}, | |
panels.extra?.let { | |
SimpleChildNavState( | |
configuration = Config.Extra(it), | |
status = Status.RESUMED, | |
) | |
}, | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment