Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decompose out multiple circuit internals for reuse + publicize CircuitContent #1024

Merged
merged 3 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,45 +87,28 @@ internal fun CircuitContent(
unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit),
context: CircuitContext,
) {
val eventListener =
remember(screen, context) {
(circuit.eventListenerFactory?.create(screen, context) ?: EventListener.NONE).also {
it.start()
}
}
val eventListener = rememberEventListener(screen, context, factory = circuit.eventListenerFactory)
DisposableEffect(eventListener, screen, context) { onDispose { eventListener.dispose() } }

val presenter =
remember(eventListener, screen, navigator, context) {
eventListener.onBeforeCreatePresenter(screen, navigator, context)
@Suppress("UNCHECKED_CAST")
(circuit.presenter(screen, navigator, context) as Presenter<CircuitUiState>?).also {
eventListener.onAfterCreatePresenter(screen, navigator, it, context)
}
}
val presenter = rememberPresenter(screen, navigator, context, eventListener, circuit::presenter)

val ui =
remember(eventListener, screen, context) {
eventListener.onBeforeCreateUi(screen, context)
circuit.ui(screen, context).also { ui -> eventListener.onAfterCreateUi(screen, ui, context) }
}
val ui = rememberUi(screen, context, eventListener, circuit::ui)

if (ui != null && presenter != null) {
@Suppress("UNCHECKED_CAST")
(CircuitContent(screen, modifier, eventListener, presenter, ui as Ui<CircuitUiState>))
(CircuitContent(screen, modifier, presenter, ui, eventListener))
} else {
eventListener.onUnavailableContent(screen, presenter, ui, context)
unavailableContent(screen, modifier)
}
}

@Composable
private fun <UiState : CircuitUiState> CircuitContent(
public fun <UiState : CircuitUiState> CircuitContent(
screen: Screen,
modifier: Modifier,
eventListener: EventListener,
presenter: Presenter<UiState>,
ui: Ui<UiState>,
eventListener: EventListener = EventListener.NONE,
) {
DisposableEffect(screen) {
eventListener.onStartPresent()
Expand All @@ -149,3 +132,71 @@ private fun <UiState : CircuitUiState> CircuitContent(
}
ui.Content(state, modifier)
}

/**
* Remembers a new [EventListener] instance for the given [screen] and [context].
*
* @param startOnInit indicates whether to call [EventListener.start] automatically after
* instantiation. True by default.
* @param factory a factory to create the [EventListener].
*/
@Suppress("NOTHING_TO_INLINE")
@Composable
public inline fun rememberEventListener(
screen: Screen,
context: CircuitContext = CircuitContext.EMPTY,
startOnInit: Boolean = true,
factory: EventListener.Factory? = null
): EventListener {
return remember(screen, context) {
(factory?.create(screen, context) ?: EventListener.NONE).also {
if (startOnInit) {
it.start()
}
}
}
}

/**
* Remembers a new [Presenter] instance for the given [screen], [navigator], [context], and
* [eventListener].
*
* @param factory a factory to create the [Presenter].
*/
@Suppress("NOTHING_TO_INLINE")
@Composable
public inline fun rememberPresenter(
screen: Screen,
navigator: Navigator = Navigator.NoOp,
context: CircuitContext = CircuitContext.EMPTY,
eventListener: EventListener = EventListener.NONE,
factory: Presenter.Factory
): Presenter<CircuitUiState>? =
remember(eventListener, screen, navigator, context) {
eventListener.onBeforeCreatePresenter(screen, navigator, context)
@Suppress("UNCHECKED_CAST")
(factory.create(screen, navigator, context) as Presenter<CircuitUiState>?).also {
eventListener.onAfterCreatePresenter(screen, navigator, it, context)
}
}

/**
* Remembers a new [Ui] instance for the given [screen], [context], and [eventListener].
*
* @param factory a factory to create the [Ui].
*/
@Suppress("NOTHING_TO_INLINE")
@Composable
public inline fun rememberUi(
screen: Screen,
context: CircuitContext = CircuitContext.EMPTY,
eventListener: EventListener = EventListener.NONE,
factory: Ui.Factory
): Ui<CircuitUiState>? =
remember(eventListener, screen, context) {
eventListener.onBeforeCreateUi(screen, context)
@Suppress("UNCHECKED_CAST")
(factory.create(screen, context) as Ui<CircuitUiState>?).also { ui ->
eventListener.onAfterCreateUi(screen, ui, context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.runtime

import com.slack.circuit.runtime.internal.NoOpMap
import kotlin.reflect.KClass

/**
Expand All @@ -16,9 +17,9 @@ public class CircuitContext
@InternalCircuitApi
public constructor(
public val parent: CircuitContext?,
) {
// Don't expose the raw map.
private val tags = mutableMapOf<KClass<*>, Any>()
private val tags: MutableMap<KClass<*>, Any> = mutableMapOf(),
) {

/** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */
public fun <T : Any> tag(type: KClass<T>): T? {
Expand Down Expand Up @@ -56,4 +57,11 @@ public constructor(
public fun clearTags() {
tags.clear()
}

public companion object {
/** An empty context */
@Suppress("UNCHECKED_CAST")
@OptIn(InternalCircuitApi::class)
public val EMPTY: CircuitContext = CircuitContext(null, NoOpMap as MutableMap<KClass<*>, Any>)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (C) 2023 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.runtime.internal

/**
* A no-op [MutableMap]. This mimics the behavior of Java's Collections.emptyMap(), which silently
* ignores mutating operations.
*/
internal object NoOpMap : MutableMap<Any, Any> {
override val size: Int = 0

@Suppress("UNCHECKED_CAST")
override val entries: MutableSet<MutableMap.MutableEntry<Any, Any>> =
NoOpSet as MutableSet<MutableMap.MutableEntry<Any, Any>>
override val keys: MutableSet<Any> = NoOpSet
override val values: MutableCollection<Any> = NoOpSet

override fun clear() {}

override fun isEmpty() = true

override fun remove(key: Any) = null

override fun putAll(from: Map<out Any, Any>) {}

override fun put(key: Any, value: Any) = null

override fun get(key: Any) = null

override fun containsValue(value: Any) = false

override fun containsKey(key: Any) = false
}

/**
* A no-op [MutableSet]. This mimics the behavior of Java's Collections.emptySet(), which silently
* ignores mutating operations.
*/
private object NoOpSet : MutableSet<Any> {
override fun add(element: Any) = false

override fun addAll(elements: Collection<Any>) = false

override val size: Int = 0

override fun clear() {}

override fun isEmpty() = true

override fun containsAll(elements: Collection<Any>) = elements.isEmpty()

override fun contains(element: Any) = false

override fun iterator() = NoOpIterator

override fun retainAll(elements: Collection<Any>) = false

override fun removeAll(elements: Collection<Any>) = false

override fun remove(element: Any) = false

object NoOpIterator : MutableIterator<Any> {
override fun hasNext() = false

override fun next(): Any {
throw UnsupportedOperationException()
}

override fun remove() {
throw UnsupportedOperationException()
}
}
}