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

commonMain.de.halfbit.componental.router.stack.StackRouter.kt Maven / Gradle / Ivy

package de.halfbit.componental.router.stack

import de.halfbit.componental.ComponentContext
import de.halfbit.componental.coroutines.createChildCoroutineScope
import de.halfbit.componental.createChildContext
import de.halfbit.componental.lifecycle.Lifecycle.State
import de.halfbit.componental.lifecycle.createMutableChildLifecycle
import de.halfbit.componental.restorator.Restorator
import de.halfbit.componental.router.RestorableRoute
import de.halfbit.componental.router.RouteNode
import de.halfbit.componental.router.Router
import de.halfbit.componental.router.RuntimeRouteNode
import de.halfbit.componental.router.stack.StackRouter.Event
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.protobuf.ProtoBuf

public data class Stack(
    val active: RouteNode,
    val inactive: List>
)

public class StackRouter : Router>() {
    public class Event(
        public val transform: (all: List) -> List,
    )
}

public fun  StackRouter.push(id: I) {
    route(
        event = Event { keys -> keys + id }
    )
}

public fun  StackRouter.pop(onLastItem: () -> Unit) {
    route(
        event = Event { ids ->
            if (ids.size > 1) {
                ids.subList(0, ids.size - 1)
            } else {
                onLastItem()
                ids
            }
        }
    )
}

public fun  ComponentContext.childStack(
    router: StackRouter,
    initial: List,
    serializer: () -> KSerializer,
    childFactory: (id: Id, context: ComponentContext) -> Child,
): StateFlow> {
    val runtimeNodes = mutableMapOf>()

    val restoredRoute: Map>? =
        restorator.restoreRoute()?.let {
            ProtoBuf.decodeFromByteArray(
                ListSerializer(RestorableRoute.serializer(serializer())), it
            ).associateBy { route -> route.id }
        }

    fun createRuntimeRouteNode(id: Id): RuntimeRouteNode {
        val tag = "id:${id::class.simpleName.toString()}"
        val childLifecycle = lifecycle.createMutableChildLifecycle()
        val restoredChildState = restoredRoute?.get(id)?.consumeChildState()
        val context = createChildContext(
            childLifecycle = childLifecycle,
            childCoroutineScope = coroutineScope.createChildCoroutineScope(tag),
            restorator = Restorator(restoredChildState)
        )
        val node = RouteNode(id, childFactory(id, context))
        return RuntimeRouteNode(
            node = node,
            lifecycle = childLifecycle,
            restorator = context.restorator,
        ).also { newNode ->
            runtimeNodes[id] = newNode
            childLifecycle.moveToState(State.Resumed)
        }
    }

    fun Collection.asStack(): Stack {

        val stackNodes = map { key ->
            runtimeNodes[key] ?: createRuntimeRouteNode(key)
        }

        stackNodes.reversed().forEachIndexed { index, node ->
            val targetState = if (index == 0) State.Resumed else State.Created
            node.lifecycle.moveToState(targetState)
        }

        val destroyedIds = runtimeNodes.keys - toSet()
        destroyedIds.forEach { key ->
            runtimeNodes[key]?.lifecycle?.moveToState(State.Destroyed)
        }

        runtimeNodes.keys.removeAll(destroyedIds)
        return stackNodes.map { it.node }.toStack()
    }

    var ids = restoredRoute?.keys?.toList() ?: initial
    restorator.storeRoute {
        ProtoBuf.encodeToByteArray(
            ListSerializer(RestorableRoute.serializer(serializer())),
            ids.map { id ->
                RestorableRoute(
                    id = id,
                    childState = runtimeNodes[id]?.restorator?.storeAll()
                )
            }
        )
    }

    return flow {
        emit(ids.asStack())
        router.events.collect { event ->
            ids = event.transform(ids)
            emit(ids.asStack())
        }
    }.stateIn(
        coroutineScope,
        SharingStarted.Eagerly,
        initial.asStack(),
    )
}

private fun  List>.toStack(): Stack {
    check(isNotEmpty()) { "List used as a stack have at least one entry" }
    return Stack(
        active = last(),
        inactive = if (size == 1) emptyList() else subList(1, lastIndex),
    )
}