Skip to content

Instantly share code, notes, and snippets.

@CXwudi
Created May 19, 2025 23:31
Show Gist options
  • Save CXwudi/03c5c7fae17518912b305f4e4b0423ea to your computer and use it in GitHub Desktop.
Save CXwudi/03c5c7fae17518912b305f4e4b0423ea to your computer and use it in GitHub Desktop.
Decompose ChildPanels with Customizable Back Handling
@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
}
}
@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