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

commonMain.com.eygraber.portal.compose.ComposePortalManager.kt Maven / Gradle / Ivy

package com.eygraber.portal.compose

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.eygraber.portal.PortalManager
import com.eygraber.portal.PortalManagerValidation
import com.eygraber.portal.PortalRendererState
import kotlinx.coroutines.flow.map

public class ComposePortalManager(
  private val defaultTransitionProvider: PortalTransitionProvider = PortalTransitionProvider.Default,
  defaultErrorHandler: ((Throwable) -> Unit)? = null,
  validation: PortalManagerValidation = PortalManagerValidation()
) : PortalManager(
  defaultErrorHandler,
  validation
) {
  private val composePortalEntriesUpdateFlow = portalEntriesUpdateFlow().map { entries ->
    entries.map(ComposePortalEntry.Companion::fromPortalEntry)
  }

  @Composable
  public fun Render() {
    val portalEntries by composePortalEntriesUpdateFlow.collectAsState(
      initial = portalEntriesUpdateFlow().value.map(ComposePortalEntry.Companion::fromPortalEntry)
    )

    for(entry in portalEntries) {
      PortalRenderer(entry)
    }
  }

  @Composable
  private fun PortalRenderer(entry: ComposePortalEntry) {
    val transitionOverride = when(entry.rendererState) {
      PortalRendererState.Added,
      PortalRendererState.Attached -> entry.enterTransitionOverride?.let { enterTransition ->
        PortalTransition(
          enterTransition,
          ExitTransition.None
        )
      }

      PortalRendererState.Detached,
      PortalRendererState.Removed -> entry.exitTransitionOverride?.let { exitTransition ->
        PortalTransition(
          EnterTransition.None,
          exitTransition
        )
      }
    }

    val (enterTransition, exitTransition) = when(transitionOverride) {
      null -> when(val transitionProvider = entry.portal) {
        is PortalTransitionProvider -> transitionProvider.provideTransitions(
          compositionState = entry.rendererState,
          isForBackstack = entry.isBackstackMutation
        )

        else -> defaultTransitionProvider.provideTransitions(
          compositionState = entry.rendererState,
          isForBackstack = entry.isBackstackMutation
        )
      }

      else -> transitionOverride
    }

    val wasContentPreviouslyVisible =
      entry.isDisappearing && entry.isAttachedToComposition ||
        entry.wasContentPreviouslyVisible

    val isContentVisible = !entry.isDisappearing && entry.isAttachedToComposition
    val visibleState = remember(
      entry.isDisappearing, isContentVisible, wasContentPreviouslyVisible
    ) {
      MutableTransitionState(wasContentPreviouslyVisible)
        .apply { targetState = isContentVisible }
    }

    // since this case won't render the AnimatedVisibility content
    // we need to handle disposing the disappearing entry here
    if(entry.isDisappearing && !entry.isAttachedToComposition) {
      DisposableEffect(Unit) {
        onDispose {
          withTransaction {
            makeEntryDisappear(entry.key)
          }
        }
      }
    }

    AnimatedVisibility(
      visibleState = visibleState,
      enter = enterTransition,
      exit = exitTransition
    ) {
      entry.portal.Render()

      if(entry.isDisappearing) {
        DisposableEffect(Unit) {
          onDispose {
            withTransaction {
              makeEntryDisappear(entry.key)
            }
          }
        }
      }
    }
  }
}

private inline val  ComposePortalEntry.isAttachedToComposition: Boolean
  get() = rendererState.isAddedOrAttached




© 2015 - 2025 Weber Informatics LLC | Privacy Policy