diff --git a/.editorconfig b/.editorconfig index 963e1b680..3e4732a78 100644 --- a/.editorconfig +++ b/.editorconfig @@ -487,7 +487,7 @@ kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ # test methods with descriptive names. ktlint_ignore_back_ticked_identifier = true -ktlint_code_style = official +ktlint_code_style = ktlint_official ktlint_function_signature_body_expression_wrapping = default ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 3 diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt index dd8613cd8..bc83e5afd 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/PerformancePoemsBrowserWorkflow.kt @@ -54,192 +54,192 @@ class PerformancePoemsBrowserWorkflow( PoemsBrowserWorkflow, StatefulWorkflow>() { - sealed class State { - object Recurse : State() + sealed class State { + object Recurse : State() - // N.B. This state is a smell. We include it to be able to mimic smells - // we encounter in real life. Best practice would be to fold it - // into [NoSelection] at the very least. - object Initializing : State() - data class ComplexCall( - val payload: Int - ) : State() + // N.B. This state is a smell. We include it to be able to mimic smells + // we encounter in real life. Best practice would be to fold it + // into [NoSelection] at the very least. + object Initializing : State() + data class ComplexCall( + val payload: Int + ) : State() - object NoSelection : State() - data class Selected(val poemIndex: Int) : State() - } + object NoSelection : State() + data class Selected(val poemIndex: Int) : State() + } - override fun initialState( - props: ConfigAndPoems, - snapshot: Snapshot? - ): State { - return if (props.first.first > 0 && - props.first.second == simulatedPerfConfig.recursionGraph.second - ) { - Recurse - } else if (simulatedPerfConfig.useInitializingState) { - Initializing - } else { - NoSelection + override fun initialState( + props: ConfigAndPoems, + snapshot: Snapshot? + ): State { + return if (props.first.first > 0 && + props.first.second == simulatedPerfConfig.recursionGraph.second + ) { + Recurse + } else if (simulatedPerfConfig.useInitializingState) { + Initializing + } else { + NoSelection + } } - } - @OptIn(WorkflowUiExperimentalApi::class) - override fun render( - renderProps: ConfigAndPoems, - renderState: State, - context: RenderContext - ): OverviewDetailScreen<*> { - when (renderState) { - is Recurse -> { - val recursiveChild = PerformancePoemsBrowserWorkflow( - simulatedPerfConfig, - poemWorkflow, - isLoading, - ) - repeat(renderProps.first.second) { breadth -> + @OptIn(WorkflowUiExperimentalApi::class) + override fun render( + renderProps: ConfigAndPoems, + renderState: State, + context: RenderContext + ): OverviewDetailScreen<*> { + when (renderState) { + is Recurse -> { + val recursiveChild = PerformancePoemsBrowserWorkflow( + simulatedPerfConfig, + poemWorkflow, + isLoading, + ) + repeat(renderProps.first.second) { breadth -> + val nextProps = renderProps.copy( + first = renderProps.first.first - 1 to breadth + ) + // When we repeat horizontally we ask the runtime to 'render' these Workflow nodes but + // we don't use their renderings in what is passed to the UI layer as this is just to + // fill out the Workflow tree to give the runtime more work to do. As such, we call + // renderChild here but we ignore the rendering returned and do no action on any Output. + // See SimulatedPerfConfig kdoc for more explanation. + context.renderChild( + child = recursiveChild, + props = nextProps, + key = "${nextProps.first},${nextProps.second}", + ) { + noAction() + } + } val nextProps = renderProps.copy( - first = renderProps.first.first - 1 to breadth + first = renderProps.first.first - 1 to renderProps.first.second ) - // When we repeat horizontally we ask the runtime to 'render' these Workflow nodes but - // we don't use their renderings in what is passed to the UI layer as this is just to - // fill out the Workflow tree to give the runtime more work to do. As such, we call - // renderChild here but we ignore the rendering returned and do no action on any Output. - // See SimulatedPerfConfig kdoc for more explanation. - context.renderChild( + return context.renderChild( child = recursiveChild, props = nextProps, key = "${nextProps.first},${nextProps.second}", ) { - noAction() - } - } - val nextProps = renderProps.copy( - first = renderProps.first.first - 1 to renderProps.first.second - ) - return context.renderChild( - child = recursiveChild, - props = nextProps, - key = "${nextProps.first},${nextProps.second}", - ) { - action { - setOutput(it) + action { + setOutput(it) + } } } - } - // Again, then entire `Initializing` state is a smell, which is most obvious from the - // use of `Worker.from { Unit }`. A Worker doing no work and only shuttling the state - // along is usually the sign you have an extraneous state that can be collapsed! - // Don't try this at home. - is Initializing -> { - context.runningWorker(TraceableWorker.from("BrowserInitializing") { Unit }, "init") { - isLoading.value = true - action { - isLoading.value = false - state = NoSelection + // Again, then entire `Initializing` state is a smell, which is most obvious from the + // use of `Worker.from { Unit }`. A Worker doing no work and only shuttling the state + // along is usually the sign you have an extraneous state that can be collapsed! + // Don't try this at home. + is Initializing -> { + context.runningWorker(TraceableWorker.from("BrowserInitializing") { Unit }, "init") { + isLoading.value = true + action { + isLoading.value = false + state = NoSelection + } } + return OverviewDetailScreen(overviewRendering = BackStackScreen(BlankScreen)) } - return OverviewDetailScreen(overviewRendering = BackStackScreen(BlankScreen)) - } - is ComplexCall, is NoSelection, is Selected -> { - if (simulatedPerfConfig.simultaneousActions > 0) { - repeat(simulatedPerfConfig.simultaneousActions) { index -> - context.runningWorker( - worker = isLoading.asTraceableWorker("SimultaneousSubscribeBrowser-$index"), - key = "Browser-$index" - ) { - noAction() + is ComplexCall, is NoSelection, is Selected -> { + if (simulatedPerfConfig.simultaneousActions > 0) { + repeat(simulatedPerfConfig.simultaneousActions) { index -> + context.runningWorker( + worker = isLoading.asTraceableWorker("SimultaneousSubscribeBrowser-$index"), + key = "Browser-$index" + ) { + noAction() + } } } - } - val poemListProps = Props( - poems = renderProps.second, - eventHandlerTag = ActionHandlingTracingInterceptor::keyForTrace - ) - val poemListRendering = context.renderChild(PoemListWorkflow, poemListProps) { selected -> - choosePoem(selected) - } - when (renderState) { - is NoSelection -> { - return OverviewDetailScreen( - overviewRendering = BackStackScreen( - poemListRendering.copy(selection = NO_POEM_SELECTED) - ) - ) + val poemListProps = Props( + poems = renderProps.second, + eventHandlerTag = ActionHandlingTracingInterceptor::keyForTrace + ) + val poemListRendering = context.renderChild(PoemListWorkflow, poemListProps) { selected -> + choosePoem(selected) } + when (renderState) { + is NoSelection -> { + return OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = NO_POEM_SELECTED) + ) + ) + } - is ComplexCall -> { - context.runningWorker( - TraceableWorker.from("ComplexCallBrowser(${renderState.payload})") { - isLoading.value = true - delay(simulatedPerfConfig.complexityDelay) - // No Output for Worker is necessary because the selected index - // is already in the state. - } - ) { - action { - isLoading.value = false - (state as? ComplexCall)?.let { currentState -> - state = if (currentState.payload != NO_POEM_SELECTED) { - Selected(currentState.payload) - } else { - NoSelection + is ComplexCall -> { + context.runningWorker( + TraceableWorker.from("ComplexCallBrowser(${renderState.payload})") { + isLoading.value = true + delay(simulatedPerfConfig.complexityDelay) + // No Output for Worker is necessary because the selected index + // is already in the state. + } + ) { + action { + isLoading.value = false + (state as? ComplexCall)?.let { currentState -> + state = if (currentState.payload != NO_POEM_SELECTED) { + Selected(currentState.payload) + } else { + NoSelection + } } } } + var poems: OverviewDetailScreen<*> = OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = renderState.payload) + ) + ) + if (renderState.payload != NO_POEM_SELECTED) { + val poem = context.renderChild( + poemWorkflow, + renderProps.second[renderState.payload] + ) { clearSelection } + poems += poem + } + return poems } - var poems: OverviewDetailScreen<*> = OverviewDetailScreen( - overviewRendering = BackStackScreen( - poemListRendering.copy(selection = renderState.payload) + + is Selected -> { + val poems = OverviewDetailScreen( + overviewRendering = BackStackScreen( + poemListRendering.copy(selection = renderState.poemIndex) + ) ) - ) - if (renderState.payload != NO_POEM_SELECTED) { val poem = context.renderChild( poemWorkflow, - renderProps.second[renderState.payload] + renderProps.second[renderState.poemIndex] ) { clearSelection } - poems += poem + return poems + poem } - return poems - } - - is Selected -> { - val poems = OverviewDetailScreen( - overviewRendering = BackStackScreen( - poemListRendering.copy(selection = renderState.poemIndex) - ) - ) - val poem = context.renderChild( - poemWorkflow, - renderProps.second[renderState.poemIndex] - ) { clearSelection } - return poems + poem - } - else -> { - throw IllegalStateException("State can't change while rendering.") + else -> { + throw IllegalStateException("State can't change while rendering.") + } } } } } - } - override fun snapshotState(state: State): Snapshot? = null + override fun snapshotState(state: State): Snapshot? = null - private fun choosePoem( - index: Int - ) = action { - state = if (simulatedPerfConfig.isComplex) { - ComplexCall(payload = index) - } else { - if (index != NO_POEM_SELECTED) { - Selected(index) + private fun choosePoem( + index: Int + ) = action { + state = if (simulatedPerfConfig.isComplex) { + ComplexCall(payload = index) } else { - NoSelection + if (index != NO_POEM_SELECTED) { + Selected(index) + } else { + NoSelection + } } } - } - private val clearSelection = choosePoem(NO_POEM_SELECTED) -} + private val clearSelection = choosePoem(NO_POEM_SELECTED) + } diff --git a/build.gradle.kts b/build.gradle.kts index dbca9edf6..ada8fa044 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -85,7 +85,7 @@ subprojects { // URL showing where the source code can be accessed through the web browser remoteUrl.set( - @Suppress("ktlint:max-line-length") + @Suppress("ktlint:standard:max-line-length") URL( "https://github.com/square/workflow-kotlin/blob/main/$modulePath/src/${dokkaSourceSet.name}" ) diff --git a/dependencies/classpath.txt b/dependencies/classpath.txt index b6b81242a..0d0a1b84c 100644 --- a/dependencies/classpath.txt +++ b/dependencies/classpath.txt @@ -57,7 +57,7 @@ com.google.code.findbugs:jsr305:3.0.2 com.google.code.gson:gson:2.8.9 com.google.crypto.tink:tink:1.7.0 com.google.dagger:dagger:2.28.3 -com.google.devtools.ksp:symbol-processing-gradle-plugin:1.8.10-1.0.9 +com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.10-1.0.13 com.google.errorprone:error_prone_annotations:2.11.0 com.google.flatbuffers:flatbuffers-java:1.12.0 com.google.guava:failureaccess:1.0.1 @@ -70,13 +70,13 @@ com.google.protobuf:protobuf-java:3.19.3 com.google.testing.platform:core-proto:0.0.8-alpha08 com.googlecode.java-diff-utils:diffutils:1.3.0 com.googlecode.juniversalchardet:juniversalchardet:1.0.3 -com.rickbusarow.ktlint:com.rickbusarow.ktlint.gradle.plugin:0.1.7 -com.rickbusarow.ktlint:ktlint-gradle-plugin:0.1.7 +com.rickbusarow.ktlint:com.rickbusarow.ktlint.gradle.plugin:0.1.8 +com.rickbusarow.ktlint:ktlint-gradle-plugin:0.1.8 com.squareup.moshi:moshi-adapters:1.15.0 com.squareup.moshi:moshi:1.15.0 -com.squareup.okhttp3:okhttp:4.10.0 -com.squareup.okio:okio-jvm:3.0.0 -com.squareup.okio:okio:3.0.0 +com.squareup.okhttp3:okhttp:4.11.0 +com.squareup.okio:okio-jvm:3.2.0 +com.squareup.okio:okio:3.2.0 com.squareup.retrofit2:converter-moshi:2.9.0 com.squareup.retrofit2:retrofit:2.9.0 com.squareup:javapoet:1.10.0 @@ -84,8 +84,8 @@ com.squareup:javawriter:2.5.0 com.sun.activation:javax.activation:1.2.0 com.sun.istack:istack-commons-runtime:3.0.8 com.sun.xml.fastinfoset:FastInfoset:1.2.16 -com.vanniktech:gradle-maven-publish-plugin:0.25.2 -com.vanniktech:nexus:0.25.2 +com.vanniktech:gradle-maven-publish-plugin:0.25.3 +com.vanniktech:nexus:0.25.3 commons-codec:commons-codec:1.11 commons-io:commons-io:2.4 commons-logging:commons-logging:1.2 @@ -130,47 +130,45 @@ org.codehaus.woodstox:stax2-api:4.2.1 org.glassfish.jaxb:jaxb-runtime:2.3.2 org.glassfish.jaxb:txw2:2.3.2 org.jdom:jdom2:2.0.6 -org.jetbrains.dokka:dokka-core:1.8.20 -org.jetbrains.dokka:dokka-gradle-plugin:1.8.20 +org.jetbrains.dokka:dokka-core:1.9.0 +org.jetbrains.dokka:dokka-gradle-plugin:1.9.0 org.jetbrains.intellij.deps:trove4j:1.0.20200330 -org.jetbrains.kotlin:kotlin-android-extensions:1.8.10 -org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.8.10 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-build-common:1.8.10 -org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.10 -org.jetbrains.kotlin:kotlin-compiler-runner:1.8.10 -org.jetbrains.kotlin:kotlin-daemon-client:1.8.10 -org.jetbrains.kotlin:kotlin-daemon-embeddable:1.8.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.8.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.8.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.8.10 -org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.8.10 -org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10 -org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.8.10 -org.jetbrains.kotlin:kotlin-native-utils:1.8.10 -org.jetbrains.kotlin:kotlin-project-model:1.8.10 +org.jetbrains.kotlin:kotlin-android-extensions:1.9.10 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-build-tools-api:1.9.10 +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.10 +org.jetbrains.kotlin:kotlin-compiler-runner:1.9.10 +org.jetbrains.kotlin:kotlin-daemon-client:1.9.10 +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.10 +org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:1.9.10 +org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.10 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.9.10 +org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.9.10 +org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.9.10 +org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10 +org.jetbrains.kotlin:kotlin-gradle-plugins-bom:1.9.10 +org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.9.10 +org.jetbrains.kotlin:kotlin-native-utils:1.9.10 +org.jetbrains.kotlin:kotlin-project-model:1.9.10 org.jetbrains.kotlin:kotlin-reflect:1.9.10 -org.jetbrains.kotlin:kotlin-scripting-common:1.8.10 -org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.8.10 -org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.8.10 -org.jetbrains.kotlin:kotlin-scripting-jvm:1.8.10 -org.jetbrains.kotlin:kotlin-serialization:1.8.10 +org.jetbrains.kotlin:kotlin-scripting-common:1.9.10 +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.10 +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.10 +org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.10 +org.jetbrains.kotlin:kotlin-serialization:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 org.jetbrains.kotlin:kotlin-stdlib:1.9.10 -org.jetbrains.kotlin:kotlin-tooling-core:1.8.10 -org.jetbrains.kotlin:kotlin-util-io:1.8.10 -org.jetbrains.kotlin:kotlin-util-klib:1.8.10 +org.jetbrains.kotlin:kotlin-tooling-core:1.9.10 +org.jetbrains.kotlin:kotlin-util-io:1.9.10 +org.jetbrains.kotlin:kotlin-util-klib:1.9.10 org.jetbrains.kotlinx:binary-compatibility-validator:0.13.2 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3 org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.6.2 org.jetbrains:annotations:13.0 -org.jetbrains:markdown-jvm:0.3.1 -org.jetbrains:markdown:0.3.1 -org.jsoup:jsoup:1.15.3 org.jvnet.staxex:stax-ex:1.8.1 org.ow2.asm:asm-analysis:9.2 org.ow2.asm:asm-commons:9.2 diff --git a/internal-testing-utils/dependencies/runtimeClasspath.txt b/internal-testing-utils/dependencies/runtimeClasspath.txt index 25dba1902..a689dbdb0 100644 --- a/internal-testing-utils/dependencies/runtimeClasspath.txt +++ b/internal-testing-utils/dependencies/runtimeClasspath.txt @@ -1,6 +1,6 @@ -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 org.jetbrains:annotations:13.0 diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index ad946cdce..1c1b92b98 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -26,15 +26,20 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== dependencies: "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" @@ -44,7 +49,20 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== @@ -90,10 +108,10 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.3.tgz#2be19e759a3dd18c79f9f436bd7363556c1a73dd" + integrity sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ== "@types/json-schema@*", "@types/json-schema@^7.0.8": version "7.0.11" @@ -105,148 +123,141 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0" integrity sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A== -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== - -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== - -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== - -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" -"@webpack-cli/configtest@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" - integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== +"@webpack-cli/configtest@^2.1.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== -"@webpack-cli/info@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" - integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== - dependencies: - envinfo "^7.7.3" +"@webpack-cli/info@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== -"@webpack-cli/serve@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" - integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== +"@webpack-cli/serve@^2.0.3": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -276,11 +287,16 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -acorn@^8.5.0, acorn@^8.7.1: +acorn@^8.7.1: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -487,16 +503,16 @@ colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -639,10 +655,10 @@ engine.io@~6.4.1: engine.io-parser "~5.0.3" ws "~8.11.0" -enhanced-resolve@^5.10.0: - version "5.14.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3" - integrity sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow== +enhanced-resolve@^5.13.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -657,10 +673,10 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" + integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== escalade@^3.1.1: version "3.1.1" @@ -783,7 +799,7 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== -format-util@1.0.5: +format-util@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== @@ -812,6 +828,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -867,6 +888,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.2.10: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -884,6 +910,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -944,10 +977,10 @@ inherits@2, inherits@2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -interpret@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" - integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== is-binary-path@~2.1.0: version "2.1.0" @@ -956,12 +989,12 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" is-extglob@^2.1.1: version "2.1.1" @@ -1050,10 +1083,10 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" -karma-chrome-launcher@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz#baca9cc071b1562a1db241827257bfe5cab597ea" - integrity sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ== +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== dependencies: which "^1.2.1" @@ -1064,12 +1097,12 @@ karma-mocha@2.0.1: dependencies: minimist "^1.2.3" -karma-sourcemap-loader@0.3.8: - version "0.3.8" - resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz#d4bae72fb7a8397328a62b75013d2df937bdcf9c" - integrity sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g== +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== dependencies: - graceful-fs "^4.1.2" + graceful-fs "^4.2.10" karma-webpack@5.0.0: version "5.0.0" @@ -1080,10 +1113,10 @@ karma-webpack@5.0.0: minimatch "^3.0.4" webpack-merge "^4.1.5" -karma@6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.0.tgz#82652dfecdd853ec227b74ed718a997028a99508" - integrity sha512-s8m7z0IF5g/bS5ONT7wsOavhW4i4aFkzD4u4wgzAQWT4HGUeWI3i21cK2Yz6jndMAeHETp5XuNsRoyGJZXVd4w== +karma@6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.2.tgz#a983f874cee6f35990c4b2dcc3d274653714de8e" + integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ== dependencies: "@colors/colors" "1.5.0" body-parser "^1.19.0" @@ -1211,12 +1244,11 @@ mkdirp@^0.5.5: dependencies: minimist "^1.2.6" -mocha@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" - integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== +mocha@10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== dependencies: - "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" chokidar "3.5.3" @@ -1431,12 +1463,12 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -rechoir@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" - integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== dependencies: - resolve "^1.9.0" + resolve "^1.20.0" require-directory@^2.1.1: version "2.1.1" @@ -1460,12 +1492,12 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.9.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -1491,7 +1523,7 @@ safe-buffer@^5.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -1500,6 +1532,15 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^3.1.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -1507,7 +1548,7 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" -serialize-javascript@^6.0.0: +serialize-javascript@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== @@ -1579,10 +1620,10 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-loader@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.0.tgz#bdc6b118bc6c87ee4d8d851f2d4efcc5abdb2ef5" - integrity sha512-i3KVgM3+QPAHNbGavK+VBq03YoJl24m9JWNbLgsjTj8aJzXG9M61bantBTNBt7CNwY2FYf+RJRYJ3pzalKjIrw== +source-map-loader@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2" + integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== dependencies: abab "^2.0.6" iconv-lite "^0.6.3" @@ -1665,24 +1706,24 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -terser-webpack-plugin@^5.1.3: - version "5.3.6" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" - integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== dependencies: - "@jridgewell/trace-mapping" "^0.3.14" + "@jridgewell/trace-mapping" "^0.3.17" jest-worker "^27.4.5" schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - terser "^5.14.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" -terser@^5.14.1: - version "5.16.4" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.4.tgz#51284b440b93242291a98f2a9903c024cfb70e6e" - integrity sha512-5yEGuZ3DZradbogeYQ1NaGz7rXVBDWujWlx1PT8efXO6Txn+eWbfKqB2bTDVmFXmePFkoLU6XI8UektMIEA0ug== +terser@^5.16.8: + version "5.22.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.22.0.tgz#4f18103f84c5c9437aafb7a14918273310a8a49d" + integrity sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" source-map-support "~0.5.20" @@ -1713,6 +1754,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + ua-parser-js@^0.7.30: version "0.7.33" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" @@ -1766,22 +1812,23 @@ watchpack@^2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -webpack-cli@4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" - integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== +webpack-cli@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.0.tgz#abc4b1f44b50250f2632d8b8b536cfe2f6257891" + integrity sha512-a7KRJnCxejFoDpYTOwzm5o21ZXMaNqtRlvS183XzGDUPRdVEzJNImcQokqYZ8BNTnk9DkKiuWxw75+DCCoZ26w== dependencies: "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^1.2.0" - "@webpack-cli/info" "^1.5.0" - "@webpack-cli/serve" "^1.7.0" + "@webpack-cli/configtest" "^2.1.0" + "@webpack-cli/info" "^2.0.1" + "@webpack-cli/serve" "^2.0.3" colorette "^2.0.14" - commander "^7.0.0" + commander "^10.0.1" cross-spawn "^7.0.3" + envinfo "^7.7.3" fastest-levenshtein "^1.0.12" import-local "^3.0.2" - interpret "^2.2.0" - rechoir "^0.7.0" + interpret "^3.1.1" + rechoir "^0.8.0" webpack-merge "^5.7.3" webpack-merge@^4.1.5: @@ -1804,22 +1851,22 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.74.0: - version "5.74.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980" - integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA== +webpack@5.82.0: + version "5.82.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.82.0.tgz#3c0d074dec79401db026b4ba0fb23d6333f88e7d" + integrity sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg== dependencies: "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" acorn "^8.7.1" acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.13.0" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" @@ -1828,9 +1875,9 @@ webpack@5.74.0: loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.1.2" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" + terser-webpack-plugin "^5.3.7" watchpack "^2.4.0" webpack-sources "^3.2.3" diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt index cee8e215a..8aa309d96 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/ScrimContainer.kt @@ -20,89 +20,91 @@ import com.squareup.workflow1.ui.WorkflowViewStub * * Able to [render][com.squareup.workflow1.ui.showRendering] [ScrimScreen]. */ -internal class ScrimContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : ViewGroup(context, attributeSet, defStyle, defStyleRes) { - private val scrim = object : View(context, attributeSet, defStyle, defStyleRes) { - init { - setBackgroundColor(ContextCompat.getColor(context, R.color.scrim)) +internal class ScrimContainer + @JvmOverloads + constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 + ) : ViewGroup(context, attributeSet, defStyle, defStyleRes) { + private val scrim = object : View(context, attributeSet, defStyle, defStyleRes) { + init { + setBackgroundColor(ContextCompat.getColor(context, R.color.scrim)) + } } - } - private val child: View - get() = getChildAt(0) - ?: error("Child must be set immediately upon creation.") + private val child: View + get() = getChildAt(0) + ?: error("Child must be set immediately upon creation.") - var isDimmed: Boolean = false - set(value) { - if (field == value) return - field = value - if (!isAttachedToWindow) updateImmediate() else updateAnimated() - } + var isDimmed: Boolean = false + set(value) { + if (field == value) return + field = value + if (!isAttachedToWindow) updateImmediate() else updateAnimated() + } - override fun onAttachedToWindow() { - updateImmediate() - super.onAttachedToWindow() - } + override fun onAttachedToWindow() { + updateImmediate() + super.onAttachedToWindow() + } - override fun addView(child: View?) { - if (scrim.parent != null) removeView(scrim) - super.addView(child) - super.addView(scrim) - } + override fun addView(child: View?) { + if (scrim.parent != null) removeView(scrim) + super.addView(child) + super.addView(scrim) + } - override fun onLayout( - changed: Boolean, - l: Int, - t: Int, - r: Int, - b: Int - ) { - child.layout(0, 0, measuredWidth, measuredHeight) - scrim.layout(0, 0, measuredWidth, measuredHeight) - } + override fun onLayout( + changed: Boolean, + l: Int, + t: Int, + r: Int, + b: Int + ) { + child.layout(0, 0, measuredWidth, measuredHeight) + scrim.layout(0, 0, measuredWidth, measuredHeight) + } - override fun onMeasure( - widthMeasureSpec: Int, - heightMeasureSpec: Int - ) { - child.measure(widthMeasureSpec, heightMeasureSpec) - scrim.measure(widthMeasureSpec, heightMeasureSpec) - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - } + override fun onMeasure( + widthMeasureSpec: Int, + heightMeasureSpec: Int + ) { + child.measure(widthMeasureSpec, heightMeasureSpec) + scrim.measure(widthMeasureSpec, heightMeasureSpec) + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } - private fun updateImmediate() { - if (isDimmed) scrim.alpha = 1f else scrim.alpha = 0f - } + private fun updateImmediate() { + if (isDimmed) scrim.alpha = 1f else scrim.alpha = 0f + } - private fun updateAnimated() { - if (isDimmed) { - ValueAnimator.ofFloat(0f, 1f) - } else { - ValueAnimator.ofFloat(1f, 0f) - }.apply { - duration = resources.getInteger(android.R.integer.config_shortAnimTime) - .toLong() - addUpdateListener { animation -> scrim.alpha = animation.animatedValue as Float } - start() + private fun updateAnimated() { + if (isDimmed) { + ValueAnimator.ofFloat(0f, 1f) + } else { + ValueAnimator.ofFloat(1f, 0f) + }.apply { + duration = resources.getInteger(android.R.integer.config_shortAnimTime) + .toLong() + addUpdateListener { animation -> scrim.alpha = animation.animatedValue as Float } + start() + } } - } - @OptIn(WorkflowUiExperimentalApi::class) - companion object : ScreenViewFactory> by fromCode( - buildView = { _, initialEnvironment, context, _ -> - val stub = WorkflowViewStub(context) - val scrimContainer = ScrimContainer(context) - scrimContainer.layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - scrimContainer.addView(stub) + @OptIn(WorkflowUiExperimentalApi::class) + companion object : ScreenViewFactory> by fromCode( + buildView = { _, initialEnvironment, context, _ -> + val stub = WorkflowViewStub(context) + val scrimContainer = ScrimContainer(context) + scrimContainer.layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) + scrimContainer.addView(stub) - ScreenViewHolder(initialEnvironment, scrimContainer) { rendering, viewEnvironment -> - stub.show(rendering.content, viewEnvironment) - scrimContainer.isDimmed = rendering.dimmed + ScreenViewHolder(initialEnvironment, scrimContainer) { rendering, viewEnvironment -> + stub.show(rendering.content, viewEnvironment) + scrimContainer.isDimmed = rendering.dimmed + } } - } - ) -} + ) + } diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonScreen.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonScreen.kt index fe00971b2..dfc911df5 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonScreen.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonScreen.kt @@ -1,4 +1,4 @@ -@file:Suppress("ktlint:filename") +@file:Suppress("ktlint:standard:filename") package com.squareup.sample.hellobackbutton diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt index a94469345..3cede8449 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemListRendering.kt @@ -1,4 +1,4 @@ -@file:Suppress("ktlint:filename") +@file:Suppress("ktlint:standard:filename") package com.squareup.sample.poetry diff --git a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt index f330af226..710b3330f 100644 --- a/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt +++ b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/RealPoemsBrowserWorkflow.kt @@ -26,47 +26,47 @@ class RealPoemsBrowserWorkflow( ) : PoemsBrowserWorkflow, StatefulWorkflow>() { - override fun initialState( - props: ConfigAndPoems, - snapshot: Snapshot? - ): SelectedPoem { - return snapshot?.bytes?.parse { source -> - source.readInt() - } ?: NO_POEM_SELECTED - } + override fun initialState( + props: ConfigAndPoems, + snapshot: Snapshot? + ): SelectedPoem { + return snapshot?.bytes?.parse { source -> + source.readInt() + } ?: NO_POEM_SELECTED + } - @OptIn(WorkflowUiExperimentalApi::class) - override fun render( - renderProps: ConfigAndPoems, - renderState: SelectedPoem, - context: RenderContext - ): OverviewDetailScreen<*> { - val poems = - context.renderChild(PoemListWorkflow, Props(poems = renderProps.second)) { selected -> - choosePoem( - selected - ) - } - .copy(selection = renderState) - .let { OverviewDetailScreen(BackStackScreen(it)) } + @OptIn(WorkflowUiExperimentalApi::class) + override fun render( + renderProps: ConfigAndPoems, + renderState: SelectedPoem, + context: RenderContext + ): OverviewDetailScreen<*> { + val poems = + context.renderChild(PoemListWorkflow, Props(poems = renderProps.second)) { selected -> + choosePoem( + selected + ) + } + .copy(selection = renderState) + .let { OverviewDetailScreen(BackStackScreen(it)) } - return if (renderState == NO_POEM_SELECTED) { - poems - } else { - val poem = - context.renderChild(poemWorkflow, renderProps.second[renderState]) { clearSelection } - poems + poem + return if (renderState == NO_POEM_SELECTED) { + poems + } else { + val poem = + context.renderChild(poemWorkflow, renderProps.second[renderState]) { clearSelection } + poems + poem + } } - } - override fun snapshotState(state: SelectedPoem): Snapshot = - Snapshot.write { sink -> sink.writeInt(state) } + override fun snapshotState(state: SelectedPoem): Snapshot = + Snapshot.write { sink -> sink.writeInt(state) } - private fun choosePoem( - index: Int - ) = action("goToPoem") { - state = index - } + private fun choosePoem( + index: Int + ) = action("goToPoem") { + state = index + } - private val clearSelection = choosePoem(NO_POEM_SELECTED) -} + private val clearSelection = choosePoem(NO_POEM_SELECTED) + } diff --git a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt index e76527597..d26869ba7 100644 --- a/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt +++ b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt @@ -8,13 +8,15 @@ import android.widget.FrameLayout /** * [FrameLayout] that can block all touch events. */ -class GlassFrameLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { +class GlassFrameLayout + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 + ) : FrameLayout(context, attrs, defStyleAttr) { - var blockTouchEvents: Boolean = false + var blockTouchEvents: Boolean = false - override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = blockTouchEvents -} + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = blockTouchEvents + } diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt index d429f1e86..32d0c2530 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt @@ -63,93 +63,93 @@ sealed class AuthResult { class RealAuthWorkflow(private val authService: AuthService) : AuthWorkflow, StatefulWorkflow>() { - override fun initialState( - props: Unit, - snapshot: Snapshot? - ): AuthState = LoginPrompt() - - override fun render( - renderProps: Unit, - renderState: AuthState, - context: RenderContext - ): BackStackScreen<*> = when (renderState) { - is LoginPrompt -> { - BackStackScreen( - LoginScreen( - renderState.errorMessage, - onLogin = context.eventHandler { email, password -> - state = when { - email.isValidEmail -> Authorizing(email, password) - else -> LoginPrompt(email.emailValidationErrorMessage) - } - }, - onCancel = context.eventHandler { setOutput(Canceled) } + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): AuthState = LoginPrompt() + + override fun render( + renderProps: Unit, + renderState: AuthState, + context: RenderContext + ): BackStackScreen<*> = when (renderState) { + is LoginPrompt -> { + BackStackScreen( + LoginScreen( + renderState.errorMessage, + onLogin = context.eventHandler { email, password -> + state = when { + email.isValidEmail -> Authorizing(email, password) + else -> LoginPrompt(email.emailValidationErrorMessage) + } + }, + onCancel = context.eventHandler { setOutput(Canceled) } + ) ) - ) - } + } - is Authorizing -> { - context.runningWorker( - authService.login(AuthRequest(renderState.email, renderState.password)) - .asWorker() - ) { handleAuthResponse(it) } + is Authorizing -> { + context.runningWorker( + authService.login(AuthRequest(renderState.email, renderState.password)) + .asWorker() + ) { handleAuthResponse(it) } - BackStackScreen( - LoginScreen(), - AuthorizingScreen("Logging in…") - ) - } - - is SecondFactorPrompt -> { - BackStackScreen( - LoginScreen(), - SecondFactorScreen( - renderState.errorMessage, - onSubmit = context.eventHandler { secondFactor -> - (state as? SecondFactorPrompt)?.let { oldState -> - state = AuthorizingSecondFactor(oldState.tempToken, secondFactor) - } - }, - onCancel = context.eventHandler { state = LoginPrompt() } + BackStackScreen( + LoginScreen(), + AuthorizingScreen("Logging in…") ) - ) - } + } - is AuthorizingSecondFactor -> { - val request = SecondFactorRequest(renderState.tempToken, renderState.secondFactor) - context.runningWorker(authService.secondFactor(request).asWorker()) { - handleSecondFactorResponse(renderState.tempToken, it) + is SecondFactorPrompt -> { + BackStackScreen( + LoginScreen(), + SecondFactorScreen( + renderState.errorMessage, + onSubmit = context.eventHandler { secondFactor -> + (state as? SecondFactorPrompt)?.let { oldState -> + state = AuthorizingSecondFactor(oldState.tempToken, secondFactor) + } + }, + onCancel = context.eventHandler { state = LoginPrompt() } + ) + ) } - BackStackScreen( - LoginScreen(), - SecondFactorScreen(), - AuthorizingScreen("Submitting one time token…") - ) + is AuthorizingSecondFactor -> { + val request = SecondFactorRequest(renderState.tempToken, renderState.secondFactor) + context.runningWorker(authService.secondFactor(request).asWorker()) { + handleSecondFactorResponse(renderState.tempToken, it) + } + + BackStackScreen( + LoginScreen(), + SecondFactorScreen(), + AuthorizingScreen("Submitting one time token…") + ) + } } - } - private fun handleAuthResponse(response: AuthResponse) = action { - when { - response.isLoginFailure -> state = LoginPrompt(response.errorMessage) - response.twoFactorRequired -> state = SecondFactorPrompt(response.token) - else -> setOutput(Authorized(response.token)) + private fun handleAuthResponse(response: AuthResponse) = action { + when { + response.isLoginFailure -> state = LoginPrompt(response.errorMessage) + response.twoFactorRequired -> state = SecondFactorPrompt(response.token) + else -> setOutput(Authorized(response.token)) + } } - } - private fun handleSecondFactorResponse(tempToken: String, response: AuthResponse) = action { - when { - response.isSecondFactorFailure -> - state = SecondFactorPrompt(tempToken, response.errorMessage) - else -> setOutput(Authorized(response.token)) + private fun handleSecondFactorResponse(tempToken: String, response: AuthResponse) = action { + when { + response.isSecondFactorFailure -> + state = SecondFactorPrompt(tempToken, response.errorMessage) + else -> setOutput(Authorized(response.token)) + } } - } - /** - * It'd be silly to restore an in progress login session, so saves nothing. - */ - override fun snapshotState(state: AuthState): Snapshot? = null -} + /** + * It'd be silly to restore an in progress login session, so saves nothing. + */ + override fun snapshotState(state: AuthState): Snapshot? = null + } private val AuthResponse.isLoginFailure: Boolean get() = token.isEmpty() && errorMessage.isNotEmpty() diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt index 5d65ef66e..462d1cc77 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt @@ -66,174 +66,174 @@ class RealRunGameWorkflow( ) : RunGameWorkflow, StatefulWorkflow() { - override fun initialState( - props: Unit, - snapshot: Snapshot? - ): RunGameState { - return snapshot?.let { RunGameState.fromSnapshot(snapshot.bytes) } - ?: NewGame() - } + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): RunGameState { + return snapshot?.let { RunGameState.fromSnapshot(snapshot.bytes) } + ?: NewGame() + } - override fun render( - renderProps: Unit, - renderState: RunGameState, - context: RenderContext - ): RunGameRendering = - when (renderState) { - is NewGame -> { - val emptyGameScreen = GamePlayScreen() - - RunGameRendering( - gameScreen = emptyGameScreen, - namePrompt = NewGameScreen( - renderState.defaultXName, - renderState.defaultOName, - onCancel = context.eventHandler { setOutput(CanceledStart) }, - onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) } + override fun render( + renderProps: Unit, + renderState: RunGameState, + context: RenderContext + ): RunGameRendering = + when (renderState) { + is NewGame -> { + val emptyGameScreen = GamePlayScreen() + + RunGameRendering( + gameScreen = emptyGameScreen, + namePrompt = NewGameScreen( + renderState.defaultXName, + renderState.defaultOName, + onCancel = context.eventHandler { setOutput(CanceledStart) }, + onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) } + ) ) - ) - } + } - is Playing -> { - // context.renderChild starts takeTurnsWorkflow, or keeps it running if it was - // already going. TakeTurnsWorkflow.render is immediately called, - // and the GamePlayScreen it renders is immediately returned. - val takeTurnsScreen = context.renderChild( - takeTurnsWorkflow, - props = renderState.resume - ?.let { TakeTurnsProps.resumeGame(renderState.playerInfo, it) } - ?: TakeTurnsProps.newGame(renderState.playerInfo) - ) { stopPlaying(it) } - - RunGameRendering(takeTurnsScreen) - } + is Playing -> { + // context.renderChild starts takeTurnsWorkflow, or keeps it running if it was + // already going. TakeTurnsWorkflow.render is immediately called, + // and the GamePlayScreen it renders is immediately returned. + val takeTurnsScreen = context.renderChild( + takeTurnsWorkflow, + props = renderState.resume + ?.let { TakeTurnsProps.resumeGame(renderState.playerInfo, it) } + ?: TakeTurnsProps.newGame(renderState.playerInfo) + ) { stopPlaying(it) } + + RunGameRendering(takeTurnsScreen) + } - is MaybeQuitting -> { - RunGameRendering( - gameScreen = GamePlayScreen( - renderState.playerInfo, - renderState.completedGame.lastTurn - ), - alerts = listOf( - maybeQuitScreen( - message = "Do you really want to concede the game?", - positive = "I Quit", - negative = "No", - confirmQuit = context.eventHandler { - (state as? MaybeQuitting)?.let { oldState -> - state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame) - } - }, - continuePlaying = context.eventHandler { - (state as? MaybeQuitting)?.let { oldState -> - state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) + is MaybeQuitting -> { + RunGameRendering( + gameScreen = GamePlayScreen( + renderState.playerInfo, + renderState.completedGame.lastTurn + ), + alerts = listOf( + maybeQuitScreen( + message = "Do you really want to concede the game?", + positive = "I Quit", + negative = "No", + confirmQuit = context.eventHandler { + (state as? MaybeQuitting)?.let { oldState -> + state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame) + } + }, + continuePlaying = context.eventHandler { + (state as? MaybeQuitting)?.let { oldState -> + state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) + } } - } + ) ) ) - ) - } + } - is MaybeQuittingForSure -> { - RunGameRendering( - gameScreen = GamePlayScreen(renderState.playerInfo, renderState.completedGame.lastTurn), - alerts = listOf( - maybeQuitScreen( - message = "Really?", - positive = "Yes!!", - negative = "Sigh, no", - confirmQuit = context.eventHandler { - (state as? MaybeQuittingForSure)?.let { oldState -> - state = GameOver(oldState.playerInfo, oldState.completedGame) + is MaybeQuittingForSure -> { + RunGameRendering( + gameScreen = GamePlayScreen(renderState.playerInfo, renderState.completedGame.lastTurn), + alerts = listOf( + maybeQuitScreen( + message = "Really?", + positive = "Yes!!", + negative = "Sigh, no", + confirmQuit = context.eventHandler { + (state as? MaybeQuittingForSure)?.let { oldState -> + state = GameOver(oldState.playerInfo, oldState.completedGame) + } + }, + continuePlaying = context.eventHandler { + (state as? MaybeQuittingForSure)?.let { oldState -> + state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) + } } - }, - continuePlaying = context.eventHandler { - (state as? MaybeQuittingForSure)?.let { oldState -> - state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn) - } - } + ) ) ) - ) - } + } - is GameOver -> { - if (renderState.syncState == SAVING) { - context.runningWorker(gameLog.logGame(renderState.completedGame).asWorker()) { - handleLogGame(it) + is GameOver -> { + if (renderState.syncState == SAVING) { + context.runningWorker(gameLog.logGame(renderState.completedGame).asWorker()) { + handleLogGame(it) + } } - } - RunGameRendering( - GameOverScreen( - renderState, - onTrySaveAgain = context.trySaveAgain(), - onPlayAgain = context.playAgain(), - onExit = context.eventHandler { setOutput(FinishedPlaying) } + RunGameRendering( + GameOverScreen( + renderState, + onTrySaveAgain = context.trySaveAgain(), + onPlayAgain = context.playAgain(), + onExit = context.eventHandler { setOutput(FinishedPlaying) } + ) ) - ) + } } - } - private fun stopPlaying(game: CompletedGame) = action { - val oldState = state as Playing - state = when (game.ending) { - Quitted -> MaybeQuitting(oldState.playerInfo, game) - else -> GameOver(oldState.playerInfo, game) + private fun stopPlaying(game: CompletedGame) = action { + val oldState = state as Playing + state = when (game.ending) { + Quitted -> MaybeQuitting(oldState.playerInfo, game) + else -> GameOver(oldState.playerInfo, game) + } } - } - private fun handleLogGame(result: GameLog.LogResult) = action { - val oldState = state as GameOver - state = when (result) { - TRY_LATER -> oldState.copy(syncState = SAVE_FAILED) - LOGGED -> oldState.copy(syncState = SAVED) + private fun handleLogGame(result: GameLog.LogResult) = action { + val oldState = state as GameOver + state = when (result) { + TRY_LATER -> oldState.copy(syncState = SAVE_FAILED) + LOGGED -> oldState.copy(syncState = SAVED) + } } - } - private fun RenderContext.playAgain() = eventHandler { - (state as? GameOver)?.let { oldState -> - val (x, o) = oldState.playerInfo - state = NewGame(x, o) + private fun RenderContext.playAgain() = eventHandler { + (state as? GameOver)?.let { oldState -> + val (x, o) = oldState.playerInfo + state = NewGame(x, o) + } } - } - private fun RenderContext.trySaveAgain() = eventHandler { - (state as? GameOver)?.let { oldState -> - check(oldState.syncState == SAVE_FAILED) { - "Should only fire trySaveAgain in syncState $SAVE_FAILED, " + - "was ${oldState.syncState}" + private fun RenderContext.trySaveAgain() = eventHandler { + (state as? GameOver)?.let { oldState -> + check(oldState.syncState == SAVE_FAILED) { + "Should only fire trySaveAgain in syncState $SAVE_FAILED, " + + "was ${oldState.syncState}" + } + state = oldState.copy(syncState = SAVING) } - state = oldState.copy(syncState = SAVING) } - } - override fun snapshotState(state: RunGameState): Snapshot = state.toSnapshot() - - private fun maybeQuitScreen( - message: String, - positive: String, - negative: String, - confirmQuit: () -> Unit, - continuePlaying: () -> Unit - ): AlertOverlay { - return AlertOverlay( - buttons = mapOf( - POSITIVE to positive, - NEGATIVE to negative - ), - message = message, - onEvent = { alertEvent -> - when (alertEvent) { - is ButtonClicked -> when (alertEvent.button) { - POSITIVE -> confirmQuit() - NEGATIVE -> continuePlaying() - NEUTRAL -> throw IllegalArgumentException() + override fun snapshotState(state: RunGameState): Snapshot = state.toSnapshot() + + private fun maybeQuitScreen( + message: String, + positive: String, + negative: String, + confirmQuit: () -> Unit, + continuePlaying: () -> Unit + ): AlertOverlay { + return AlertOverlay( + buttons = mapOf( + POSITIVE to positive, + NEGATIVE to negative + ), + message = message, + onEvent = { alertEvent -> + when (alertEvent) { + is ButtonClicked -> when (alertEvent.button) { + POSITIVE -> confirmQuit() + NEGATIVE -> continuePlaying() + NEUTRAL -> throw IllegalArgumentException() + } + + Canceled -> continuePlaying() } - - Canceled -> continuePlaying() } - } - ) + ) + } } -} diff --git a/trace-encoder/api/trace-encoder.api b/trace-encoder/api/trace-encoder.api index cb25a68d8..6facb449a 100644 --- a/trace-encoder/api/trace-encoder.api +++ b/trace-encoder/api/trace-encoder.api @@ -120,6 +120,7 @@ public final class com/squareup/tracing/TraceEvent$Instant$InstantScope : java/l public static final field GLOBAL Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; public static final field PROCESS Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; public static final field THREAD Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; public static fun values ()[Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; } diff --git a/trace-encoder/dependencies/runtimeClasspath.txt b/trace-encoder/dependencies/runtimeClasspath.txt index aaeae6d22..1562a659c 100644 --- a/trace-encoder/dependencies/runtimeClasspath.txt +++ b/trace-encoder/dependencies/runtimeClasspath.txt @@ -2,12 +2,12 @@ com.squareup.moshi:moshi-adapters:1.15.0 com.squareup.moshi:moshi:1.15.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 -org.jetbrains.kotlin:kotlin-stdlib:1.8.21 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt index b1b31b28e..d8cb7353d 100644 --- a/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-config/config-android/dependencies/releaseRuntimeClasspath.txt @@ -1,11 +1,11 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt index b1b31b28e..d8cb7353d 100644 --- a/workflow-config/config-jvm/dependencies/runtimeClasspath.txt +++ b/workflow-config/config-jvm/dependencies/runtimeClasspath.txt @@ -1,11 +1,11 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-core/dependencies/jsRuntimeClasspath.txt b/workflow-core/dependencies/jsRuntimeClasspath.txt index f94d8469c..6dd90ac24 100644 --- a/workflow-core/dependencies/jsRuntimeClasspath.txt +++ b/workflow-core/dependencies/jsRuntimeClasspath.txt @@ -1,10 +1,11 @@ com.squareup.okio:okio-js:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-js:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-dom-api-compat:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-js:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 org.jetbrains.kotlin:kotlinx-atomicfu-runtime:1.8.20 -org.jetbrains.kotlinx:atomicfu-js:0.20.2 -org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlinx:atomicfu-js:0.21.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:13.0 diff --git a/workflow-core/dependencies/jvmRuntimeClasspath.txt b/workflow-core/dependencies/jvmRuntimeClasspath.txt index 66d9bc7ac..3ced6a669 100644 --- a/workflow-core/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-core/dependencies/jvmRuntimeClasspath.txt @@ -1,10 +1,10 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-core/dependencies/runtimeClasspath.txt b/workflow-core/dependencies/runtimeClasspath.txt index c0a590ef4..d8cb7353d 100644 --- a/workflow-core/dependencies/runtimeClasspath.txt +++ b/workflow-core/dependencies/runtimeClasspath.txt @@ -1,11 +1,11 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt index 456b0f530..cf3d2173a 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt @@ -1,5 +1,5 @@ // Type variance issue: https://github.com/square/workflow-kotlin/issues/891 -@file:Suppress("EXPERIMENTAL_API_USAGE", "TYPE_VARIANCE_CONFLICT_WARNING") +@file:Suppress("EXPERIMENTAL_API_USAGE") @file:JvmMultifileClass @file:JvmName("Workflows") @@ -132,7 +132,9 @@ public interface BaseRenderContext { */ public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.() -> Unit + // Type variance issue: https://github.com/square/workflow-kotlin/issues/891 + update: + WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.() -> Unit ): () -> Unit { return { actionSink.send(action(name, update)) @@ -141,7 +143,9 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(EventT) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + EventT + ) -> Unit ): (EventT) -> Unit { return { event -> actionSink.send(action(name) { update(event) }) @@ -150,7 +154,10 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(E1, E2) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + E1, + E2 + ) -> Unit ): (E1, E2) -> Unit { return { e1, e2 -> actionSink.send(action(name) { update(e1, e2) }) @@ -159,7 +166,11 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(E1, E2, E3) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + E1, + E2, + E3 + ) -> Unit ): (E1, E2, E3) -> Unit { return { e1, e2, e3 -> actionSink.send(action(name) { update(e1, e2, e3) }) @@ -168,7 +179,12 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(E1, E2, E3, E4) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + E1, + E2, + E3, + E4 + ) -> Unit ): (E1, E2, E3, E4) -> Unit { return { e1, e2, e3, e4 -> actionSink.send(action(name) { update(e1, e2, e3, e4) }) @@ -177,7 +193,13 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(E1, E2, E3, E4, E5) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + E1, + E2, + E3, + E4, + E5 + ) -> Unit ): (E1, E2, E3, E4, E5) -> Unit { return { e1, e2, e3, e4, e5 -> actionSink.send(action(name) { update(e1, e2, e3, e4, e5) }) @@ -186,7 +208,14 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(E1, E2, E3, E4, E5, E6) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + E1, + E2, + E3, + E4, + E5, + E6 + ) -> Unit ): (E1, E2, E3, E4, E5, E6) -> Unit { return { e1, e2, e3, e4, e5, e6 -> actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6) }) @@ -195,7 +224,15 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(E1, E2, E3, E4, E5, E6, E7) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + E1, + E2, + E3, + E4, + E5, + E6, + E7 + ) -> Unit ): (E1, E2, E3, E4, E5, E6, E7) -> Unit { return { e1, e2, e3, e4, e5, e6, e7 -> actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7) }) @@ -204,7 +241,16 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction.Updater.(E1, E2, E3, E4, E5, E6, E7, E8) -> Unit + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>.Updater.( + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8 + ) -> Unit ): (E1, E2, E3, E4, E5, E6, E7, E8) -> Unit { return { e1, e2, e3, e4, e5, e6, e7, e8 -> actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7, e8) }) @@ -213,7 +259,7 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT> .Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit ): (E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit { return { e1, e2, e3, e4, e5, e6, e7, e8, e9 -> @@ -223,7 +269,7 @@ public interface BaseRenderContext { public fun eventHandler( name: () -> String = { "eventHandler" }, - update: WorkflowAction + update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT> .Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit ): (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit { return { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 -> diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt index 0aa2bbba7..6b2daabf1 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt @@ -16,66 +16,66 @@ import kotlin.jvm.JvmStatic * worrying about performing unnecessary serialization work. */ public class Snapshot -private constructor(private val toByteString: () -> ByteString) { - - public companion object { - @JvmStatic - public fun of(string: String): Snapshot = - Snapshot { string.encodeUtf8() } - - @JvmStatic - public fun of(byteString: ByteString): Snapshot = - Snapshot { byteString } - - @JvmStatic - public fun of(lazy: () -> ByteString): Snapshot = - Snapshot(lazy) - - @JvmStatic - public fun of(integer: Int): Snapshot { - return Snapshot { - with(Buffer()) { - writeInt(integer) - readByteString() + private constructor(private val toByteString: () -> ByteString) { + + public companion object { + @JvmStatic + public fun of(string: String): Snapshot = + Snapshot { string.encodeUtf8() } + + @JvmStatic + public fun of(byteString: ByteString): Snapshot = + Snapshot { byteString } + + @JvmStatic + public fun of(lazy: () -> ByteString): Snapshot = + Snapshot(lazy) + + @JvmStatic + public fun of(integer: Int): Snapshot { + return Snapshot { + with(Buffer()) { + writeInt(integer) + readByteString() + } } } + + /** Create a snapshot by writing to a nice ergonomic [BufferedSink]. */ + @JvmStatic + public fun write(lazy: (BufferedSink) -> Unit): Snapshot = + of { + Buffer().apply(lazy) + .readByteString() + } } - /** Create a snapshot by writing to a nice ergonomic [BufferedSink]. */ - @JvmStatic - public fun write(lazy: (BufferedSink) -> Unit): Snapshot = - of { - Buffer().apply(lazy) - .readByteString() - } + @get:JvmName("bytes") + public val bytes: ByteString by lazy { toByteString() } + + /** + * Returns a `String` describing the [bytes] of this `Snapshot`. + * + * **This method forces serialization, calling it may be expensive.** + */ + override fun toString(): String = "Snapshot($bytes)" + + /** + * Compares `Snapshot`s by comparing their [bytes]. + * + * **This method forces serialization, calling it may be expensive.** + */ + override fun equals(other: Any?): Boolean = + (other as? Snapshot)?.let { bytes == it.bytes } ?: false + + /** + * Calculates hashcode using [bytes]. + * + * **This method forces serialization, calling it may be expensive.** + */ + override fun hashCode(): Int = bytes.hashCode() } - @get:JvmName("bytes") - public val bytes: ByteString by lazy { toByteString() } - - /** - * Returns a `String` describing the [bytes] of this `Snapshot`. - * - * **This method forces serialization, calling it may be expensive.** - */ - override fun toString(): String = "Snapshot($bytes)" - - /** - * Compares `Snapshot`s by comparing their [bytes]. - * - * **This method forces serialization, calling it may be expensive.** - */ - override fun equals(other: Any?): Boolean = - (other as? Snapshot)?.let { bytes == it.bytes } ?: false - - /** - * Calculates hashcode using [bytes]. - * - * **This method forces serialization, calling it may be expensive.** - */ - override fun hashCode(): Int = bytes.hashCode() -} - public fun BufferedSink.writeNullable( obj: T?, writer: BufferedSink.(T) -> Unit diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt index 983f7ac5d..8931db92f 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt @@ -34,34 +34,34 @@ internal class WorkerWorkflow( ) : StatefulWorkflow, Int, OutputT, Unit>(), ImpostorWorkflow { - override val realIdentifier: WorkflowIdentifier = unsnapshottableIdentifier(workerType) - override fun describeRealIdentifier(): String = workerType.toString() + override val realIdentifier: WorkflowIdentifier = unsnapshottableIdentifier(workerType) + override fun describeRealIdentifier(): String = workerType.toString() - override fun initialState( - props: Worker, - snapshot: Snapshot? - ): Int = 0 + override fun initialState( + props: Worker, + snapshot: Snapshot? + ): Int = 0 - override fun onPropsChanged( - old: Worker, - new: Worker, - state: Int - ): Int = if (!old.doesSameWorkAs(new)) state + 1 else state + override fun onPropsChanged( + old: Worker, + new: Worker, + state: Int + ): Int = if (!old.doesSameWorkAs(new)) state + 1 else state - override fun render( - renderProps: Worker, - renderState: Int, - context: RenderContext - ) { - // Scope the side effect coroutine to the state value, so the worker will be re-started when - // it changes (such that doesSameWorkAs returns false above). - context.runningSideEffect(renderState.toString()) { - runWorker(renderProps, key, context.actionSink) + override fun render( + renderProps: Worker, + renderState: Int, + context: RenderContext + ) { + // Scope the side effect coroutine to the state value, so the worker will be re-started when + // it changes (such that doesSameWorkAs returns false above). + context.runningSideEffect(renderState.toString()) { + runWorker(renderProps, key, context.actionSink) + } } - } - override fun snapshotState(state: Int): Snapshot? = null -} + override fun snapshotState(state: Int): Snapshot? = null + } /** * Does the actual running of a worker passed to [BaseRenderContext.runningWorker] by setting up the diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt index 3ced69868..6d96206a5 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt @@ -162,43 +162,45 @@ public object ActionsExhausted : ActionProcessingResult * Also note that since we have decided to allow destructuring and implemented componentN() * functions, we should only ever add new properties to the end of this constructor. */ -public class ActionApplied @JvmOverloads constructor( - public val output: WorkflowOutput?, - public val stateChanged: Boolean = false, -) : ActionProcessingResult { - public override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false +public class ActionApplied + @JvmOverloads + constructor( + public val output: WorkflowOutput?, + public val stateChanged: Boolean = false, + ) : ActionProcessingResult { + public override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false - other as ActionApplied<*> + other as ActionApplied<*> - if (output != other.output) return false - if (stateChanged != other.stateChanged) return false + if (output != other.output) return false + if (stateChanged != other.stateChanged) return false - return true - } + return true + } - public override fun hashCode(): Int { - var result = output?.hashCode() ?: 0 - result = 31 * result + stateChanged.hashCode() - return result - } + public override fun hashCode(): Int { + var result = output?.hashCode() ?: 0 + result = 31 * result + stateChanged.hashCode() + return result + } - public override fun toString(): String { - return "ActionApplied(output=$output, stateChanged=$stateChanged)" - } + public override fun toString(): String { + return "ActionApplied(output=$output, stateChanged=$stateChanged)" + } - /** - * Only add to the end of this function to avoid binary compatibility issues. - */ - @JvmOverloads - public fun copy( - output: WorkflowOutput<@UnsafeVariance OutputT>? = this.output, - stateChanged: Boolean = this.stateChanged - ): ActionApplied { - return ActionApplied(output, stateChanged) - } + /** + * Only add to the end of this function to avoid binary compatibility issues. + */ + @JvmOverloads + public fun copy( + output: WorkflowOutput<@UnsafeVariance OutputT>? = this.output, + stateChanged: Boolean = this.stateChanged + ): ActionApplied { + return ActionApplied(output, stateChanged) + } - public fun component1(): WorkflowOutput? = output - public fun component2(): Boolean = stateChanged -} + public fun component1(): WorkflowOutput? = output + public fun component2(): Boolean = stateChanged + } diff --git a/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt index b3bb5b7fd..669d4834a 100644 --- a/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt +++ b/workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt @@ -78,8 +78,9 @@ internal class WorkflowIdentifierTest { assertEquals(id.hashCode(), restoredId.hashCode()) } + @Suppress("ktlint:standard:max-line-length") @Test - fun impostor_identifier_restored_from_source_is_not_equal_to_impostor_with_different_proxied_class() { // ktlint-disable max-line-length + fun impostor_identifier_restored_from_source_is_not_equal_to_impostor_with_different_proxied_class() { val id1 = TestImpostor1(TestWorkflow1).identifier val id2 = TestImpostor1(TestWorkflow2).identifier val serializedId = id1.toByteStringOrNull()!! @@ -87,8 +88,9 @@ internal class WorkflowIdentifierTest { assertNotEquals(id2, restoredId) } + @Suppress("ktlint:standard:max-line-length") @Test - fun impostor_identifier_restored_from_source_is_not_equal_to_different_impostor_with_same_proxied_class() { // ktlint-disable max-line-length + fun impostor_identifier_restored_from_source_is_not_equal_to_different_impostor_with_same_proxied_class() { val id1 = TestImpostor1(TestWorkflow1).identifier val id2 = TestImpostor2(TestWorkflow1).identifier val serializedId = id1.toByteStringOrNull()!! diff --git a/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api index 9d3d2f3ed..f177256f2 100644 --- a/workflow-runtime/api/workflow-runtime.api +++ b/workflow-runtime/api/workflow-runtime.api @@ -26,6 +26,7 @@ public final class com/squareup/workflow1/RuntimeConfigOptions : java/lang/Enum public static final field CONFLATE_STALE_RENDERINGS Lcom/squareup/workflow1/RuntimeConfigOptions; public static final field Companion Lcom/squareup/workflow1/RuntimeConfigOptions$Companion; public static final field RENDER_ONLY_WHEN_STATE_CHANGES Lcom/squareup/workflow1/RuntimeConfigOptions; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/RuntimeConfigOptions; public static fun values ()[Lcom/squareup/workflow1/RuntimeConfigOptions; } diff --git a/workflow-runtime/dependencies/jsRuntimeClasspath.txt b/workflow-runtime/dependencies/jsRuntimeClasspath.txt index f94d8469c..6dd90ac24 100644 --- a/workflow-runtime/dependencies/jsRuntimeClasspath.txt +++ b/workflow-runtime/dependencies/jsRuntimeClasspath.txt @@ -1,10 +1,11 @@ com.squareup.okio:okio-js:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-js:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-dom-api-compat:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-js:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 org.jetbrains.kotlin:kotlinx-atomicfu-runtime:1.8.20 -org.jetbrains.kotlinx:atomicfu-js:0.20.2 -org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlinx:atomicfu-js:0.21.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:13.0 diff --git a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt index 42f31697d..3ced6a669 100644 --- a/workflow-runtime/dependencies/jvmRuntimeClasspath.txt +++ b/workflow-runtime/dependencies/jvmRuntimeClasspath.txt @@ -1,10 +1,10 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt index 21ee5ca14..7580098f9 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptor.kt @@ -38,7 +38,8 @@ internal class ChainedWorkflowInterceptor( session: WorkflowSession ): S { val chainedProceed = interceptors.foldRight(proceed) { workflowInterceptor, proceedAcc -> - { props, snapshot, workflowScope -> + { + props, snapshot, workflowScope -> workflowInterceptor.onInitialState(props, snapshot, workflowScope, proceedAcc, session) } } @@ -53,7 +54,8 @@ internal class ChainedWorkflowInterceptor( session: WorkflowSession ): S { val chainedProceed = interceptors.foldRight(proceed) { workflowInterceptor, proceedAcc -> - { old, new, state -> + { + old, new, state -> workflowInterceptor.onPropsChanged(old, new, state, proceedAcc, session) } } @@ -66,7 +68,8 @@ internal class ChainedWorkflowInterceptor( session: WorkflowSession ): RenderingAndSnapshot { val chainedProceed = interceptors.foldRight(proceed) { workflowInterceptor, proceedAcc -> - { renderProps -> + { + renderProps -> workflowInterceptor.onRenderAndSnapshot(renderProps, proceedAcc, session) } } @@ -81,7 +84,8 @@ internal class ChainedWorkflowInterceptor( session: WorkflowSession ): R { val chainedProceed = interceptors.foldRight(proceed) { workflowInterceptor, proceedAcc -> - { props, state, outerContextInterceptor -> + { + props, state, outerContextInterceptor -> workflowInterceptor.onRender( props, state, @@ -115,7 +119,8 @@ internal class ChainedWorkflowInterceptor( session: WorkflowSession ): Snapshot? { val chainedProceed = interceptors.foldRight(proceed) { workflowInterceptor, proceedAcc -> - { state -> + { + state -> workflowInterceptor.onSnapshotState(state, proceedAcc, session) } } diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt index 45ce2dc9a..f6edca98f 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/RealRenderContextTest.kt @@ -268,8 +268,14 @@ internal class RealRenderContextTest { @Test fun eventHandler7_gets_event() { val context = createdPoisonedContext() val sink = - context.eventHandler { a: String, b: String, c: String, d: String, e: String, f: String, - g: String -> + context.eventHandler { + a: String, + b: String, + c: String, + d: String, + e: String, + f: String, + g: String -> setOutput(a + b + c + d + e + f + g) } // Enable sink sends. @@ -287,8 +293,15 @@ internal class RealRenderContextTest { @Test fun eventHandler8_gets_event() { val context = createdPoisonedContext() val sink = - context.eventHandler { a: String, b: String, c: String, d: String, e: String, f: String, - g: String, h: String -> + context.eventHandler { + a: String, + b: String, + c: String, + d: String, + e: String, + f: String, + g: String, + h: String -> setOutput(a + b + c + d + e + f + g + h) } // Enable sink sends. @@ -306,8 +319,16 @@ internal class RealRenderContextTest { @Test fun eventHandler9_gets_event() { val context = createdPoisonedContext() val sink = - context.eventHandler { a: String, b: String, c: String, d: String, e: String, f: String, - g: String, h: String, i: String -> + context.eventHandler { + a: String, + b: String, + c: String, + d: String, + e: String, + f: String, + g: String, + h: String, + i: String -> setOutput(a + b + c + d + e + f + g + h + i) } // Enable sink sends. @@ -325,8 +346,17 @@ internal class RealRenderContextTest { @Test fun eventHandler10_gets_event() { val context = createdPoisonedContext() val sink = - context.eventHandler { a: String, b: String, c: String, d: String, e: String, f: String, - g: String, h: String, i: String, j: String -> + context.eventHandler { + a: String, + b: String, + c: String, + d: String, + e: String, + f: String, + g: String, + h: String, + i: String, + j: String -> setOutput(a + b + c + d + e + f + g + h + i + j) } // Enable sink sends. diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt index 1301d7c8e..9da043d20 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/WorkflowNodeTest.kt @@ -90,7 +90,7 @@ internal class WorkflowNodeTest { return """ props:$renderProps state:$renderState - """.trimIndent() + """.trimIndent() } } diff --git a/workflow-rx2/dependencies/runtimeClasspath.txt b/workflow-rx2/dependencies/runtimeClasspath.txt index c84b70adf..1b8022059 100644 --- a/workflow-rx2/dependencies/runtimeClasspath.txt +++ b/workflow-rx2/dependencies/runtimeClasspath.txt @@ -1,15 +1,15 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 io.reactivex.rxjava2:rxjava:2.2.21 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.7.3 org.jetbrains:annotations:23.0.0 org.reactivestreams:reactive-streams:1.0.4 diff --git a/workflow-testing/dependencies/runtimeClasspath.txt b/workflow-testing/dependencies/runtimeClasspath.txt index 1598c0112..1de4577fd 100644 --- a/workflow-testing/dependencies/runtimeClasspath.txt +++ b/workflow-testing/dependencies/runtimeClasspath.txt @@ -2,15 +2,15 @@ app.cash.turbine:turbine-jvm:1.0.0 app.cash.turbine:turbine:1.0.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-reflect:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-reflect:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt index 1c0afb5fd..aa3079f88 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RealRenderTester.kt @@ -68,269 +68,269 @@ internal class RealRenderTester( RenderTestResult, Sink> { - internal sealed class Expectation { - abstract fun describe(): String + internal sealed class Expectation { + abstract fun describe(): String - open val output: WorkflowOutput? = null + open val output: WorkflowOutput? = null - data class ExpectedWorkflow( - val matcher: (RenderChildInvocation) -> ChildWorkflowMatch, - val exactMatch: Boolean, - val description: String - ) : Expectation() { - override fun describe(): String = description - } + data class ExpectedWorkflow( + val matcher: (RenderChildInvocation) -> ChildWorkflowMatch, + val exactMatch: Boolean, + val description: String + ) : Expectation() { + override fun describe(): String = description + } - data class ExpectedWorker( - val matchesWhen: (otherWorker: Worker<*>) -> Boolean, - val key: String, - override val output: WorkflowOutput?, - val description: String - ) : Expectation() { - override fun describe(): String = description.ifBlank { "worker key=$key, output=$output" } - } + data class ExpectedWorker( + val matchesWhen: (otherWorker: Worker<*>) -> Boolean, + val key: String, + override val output: WorkflowOutput?, + val description: String + ) : Expectation() { + override fun describe(): String = description.ifBlank { "worker key=$key, output=$output" } + } - data class ExpectedSideEffect( - val matcher: (String) -> Boolean, - val exactMatch: Boolean, - val description: String - ) : Expectation() { - override fun describe(): String = description + data class ExpectedSideEffect( + val matcher: (String) -> Boolean, + val exactMatch: Boolean, + val description: String + ) : Expectation() { + override fun describe(): String = description + } } - } - - private var explicitWorkerExpectationsRequired: Boolean = false - private var explicitSideEffectExpectationsRequired: Boolean = false - private val stateAndOutput: Pair?> by lazy { - val action = processedAction ?: noAction() - val (state, actionApplied) = action.applyTo(props, state) - state to actionApplied.output - } - override val actionSink: Sink> get() = this - - override fun expectWorkflow( - description: String, - exactMatch: Boolean, - matcher: (RenderChildInvocation) -> ChildWorkflowMatch - ): RenderTester = apply { - expectations += ExpectedWorkflow(matcher, exactMatch, description) - } - - override fun expectSideEffect( - description: String, - exactMatch: Boolean, - matcher: (key: String) -> Boolean - ): RenderTester = apply { - expectations += ExpectedSideEffect(matcher, exactMatch, description) - } - override fun render( - block: (RenderingT) -> Unit - ): RenderTestResult { - if (!explicitWorkerExpectationsRequired) { - // Allow unexpected workers. - expectWorker(description = "unexpected worker", exactMatch = false) { _, _, _ -> true } + private var explicitWorkerExpectationsRequired: Boolean = false + private var explicitSideEffectExpectationsRequired: Boolean = false + private val stateAndOutput: Pair?> by lazy { + val action = processedAction ?: noAction() + val (state, actionApplied) = action.applyTo(props, state) + state to actionApplied.output } - - if (!explicitSideEffectExpectationsRequired) { - // Allow unexpected side effects. - expectSideEffect(description = "unexpected side effect", exactMatch = false) { true } + override val actionSink: Sink> get() = this + + override fun expectWorkflow( + description: String, + exactMatch: Boolean, + matcher: (RenderChildInvocation) -> ChildWorkflowMatch + ): RenderTester = apply { + expectations += ExpectedWorkflow(matcher, exactMatch, description) } - // Clone the expectations to run a "dry" render pass. - val noopContext = deepCloneForRender() - workflow.render(props, state, RenderContext(noopContext, workflow)) + override fun expectSideEffect( + description: String, + exactMatch: Boolean, + matcher: (key: String) -> Boolean + ): RenderTester = apply { + expectations += ExpectedSideEffect(matcher, exactMatch, description) + } - workflow.render(props, state, RenderContext(this, workflow)) - .also(block) + override fun render( + block: (RenderingT) -> Unit + ): RenderTestResult { + if (!explicitWorkerExpectationsRequired) { + // Allow unexpected workers. + expectWorker(description = "unexpected worker", exactMatch = false) { _, _, _ -> true } + } - // Ensure all exact matches were consumed. - val unconsumedExactMatches = expectations.filter { - when (it) { - is ExpectedWorkflow -> it.exactMatch - // Workers are always exact matches. - is ExpectedWorker -> true - is ExpectedSideEffect -> it.exactMatch + if (!explicitSideEffectExpectationsRequired) { + // Allow unexpected side effects. + expectSideEffect(description = "unexpected side effect", exactMatch = false) { true } } - } - if (unconsumedExactMatches.isNotEmpty()) { - throw AssertionError( - "Expected ${unconsumedExactMatches.size} more workflows, workers, or " + - "side effects to be run:\n" + - unconsumedExactMatches.joinToString(separator = "\n") { " ${it.describe()}" } - ) - } - return this - } + // Clone the expectations to run a "dry" render pass. + val noopContext = deepCloneForRender() + workflow.render(props, state, RenderContext(noopContext, workflow)) + + workflow.render(props, state, RenderContext(this, workflow)) + .also(block) + + // Ensure all exact matches were consumed. + val unconsumedExactMatches = expectations.filter { + when (it) { + is ExpectedWorkflow -> it.exactMatch + // Workers are always exact matches. + is ExpectedWorker -> true + is ExpectedSideEffect -> it.exactMatch + } + } + if (unconsumedExactMatches.isNotEmpty()) { + throw AssertionError( + "Expected ${unconsumedExactMatches.size} more workflows, workers, or " + + "side effects to be run:\n" + + unconsumedExactMatches.joinToString(separator = "\n") { " ${it.describe()}" } + ) + } - override fun renderChild( - child: Workflow, - props: ChildPropsT, - key: String, - handler: (ChildOutputT) -> WorkflowAction - ): ChildRenderingT { - val identifierPair = Pair(child.identifier, key) - require(identifierPair !in renderedChildren) { - "Expected keys to be unique for ${child.identifier}: key=\"$key\"" + return this } - renderedChildren += identifierPair - val description = buildString { - append("child ") - append(child.identifier) - if (key.isNotEmpty()) { - append(" with key \"$key\"") + override fun renderChild( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): ChildRenderingT { + val identifierPair = Pair(child.identifier, key) + require(identifierPair !in renderedChildren) { + "Expected keys to be unique for ${child.identifier}: key=\"$key\"" } - } - val invocation = createRenderChildInvocation(child, props, key) - val matches = expectations.filterIsInstance() - .mapNotNull { - val matchResult = it.matcher(invocation) - if (matchResult is Matched) Pair(it, matchResult) else null + renderedChildren += identifierPair + + val description = buildString { + append("child ") + append(child.identifier) + if (key.isNotEmpty()) { + append(" with key \"$key\"") + } + } + val invocation = createRenderChildInvocation(child, props, key) + val matches = expectations.filterIsInstance() + .mapNotNull { + val matchResult = it.matcher(invocation) + if (matchResult is Matched) Pair(it, matchResult) else null + } + if (matches.isEmpty()) { + throw AssertionError("Tried to render unexpected $description") + } + + val exactMatches = matches.filter { it.first.exactMatch } + val (_, match) = when { + exactMatches.size == 1 -> { + exactMatches.single() + .also { (expected, _) -> + expectations -= expected + consumedExpectations += expected + } + } + + exactMatches.size > 1 -> { + throw AssertionError( + "Multiple expectations matched $description:\n" + + exactMatches.joinToString(separator = "\n") { " ${it.first.describe()}" } + ) + } + // Inexact matches are not consumable. + else -> matches.first() + } + + if (match.output != null) { + check(processedAction == null) { + "Expected only one output to be expected: $description expected to emit " + + "${match.output.value} but $processedAction was already processed." + } + @Suppress("UNCHECKED_CAST") + processedAction = handler(match.output.value as ChildOutputT) } - if (matches.isEmpty()) { - throw AssertionError("Tried to render unexpected $description") + + @Suppress("UNCHECKED_CAST") + return match.childRendering as ChildRenderingT } - val exactMatches = matches.filter { it.first.exactMatch } - val (_, match) = when { - exactMatches.size == 1 -> { - exactMatches.single() - .also { (expected, _) -> - expectations -= expected - consumedExpectations += expected - } + override fun runningSideEffect( + key: String, + sideEffect: suspend CoroutineScope.() -> Unit + ) { + require(key !in ranSideEffects) { "Expected side effect keys to be unique: \"$key\"" } + ranSideEffects += key + + val description = "side effect with key \"$key\"" + + val matches = expectations.filterIsInstance() + .mapNotNull { + if (it.matcher(key)) it else null + } + if (matches.isEmpty()) { + throw AssertionError("Tried to run unexpected $description") } + val exactMatches = matches.filter { it.exactMatch } - exactMatches.size > 1 -> { + if (exactMatches.size > 1) { throw AssertionError( "Multiple expectations matched $description:\n" + - exactMatches.joinToString(separator = "\n") { " ${it.first.describe()}" } + matches.joinToString(separator = "\n") { " ${it.describe()}" } ) } + // Inexact matches are not consumable. - else -> matches.first() + exactMatches.singleOrNull() + ?.let { expected -> + expectations -= expected + consumedExpectations += expected + } } - if (match.output != null) { - check(processedAction == null) { - "Expected only one output to be expected: $description expected to emit " + - "${match.output.value} but $processedAction was already processed." - } - @Suppress("UNCHECKED_CAST") - processedAction = handler(match.output.value as ChildOutputT) + override fun requireExplicitWorkerExpectations(): + RenderTester = this.apply { + explicitWorkerExpectationsRequired = true } - @Suppress("UNCHECKED_CAST") - return match.childRendering as ChildRenderingT - } - - override fun runningSideEffect( - key: String, - sideEffect: suspend CoroutineScope.() -> Unit - ) { - require(key !in ranSideEffects) { "Expected side effect keys to be unique: \"$key\"" } - ranSideEffects += key - - val description = "side effect with key \"$key\"" + override fun requireExplicitSideEffectExpectations(): + RenderTester = this.apply { + explicitSideEffectExpectationsRequired = true + } - val matches = expectations.filterIsInstance() - .mapNotNull { - if (it.matcher(key)) it else null + override fun send(value: WorkflowAction) { + checkNoOutputs() + check(processedAction == null) { + "Tried to send action to sink after another action was already processed:\n" + + " processed action=$processedAction\n" + + " attempted action=$value" } - if (matches.isEmpty()) { - throw AssertionError("Tried to run unexpected $description") + processedAction = value } - val exactMatches = matches.filter { it.exactMatch } - if (exactMatches.size > 1) { - throw AssertionError( - "Multiple expectations matched $description:\n" + - matches.joinToString(separator = "\n") { " ${it.describe()}" } - ) + override fun verifyAction( + block: (WorkflowAction) -> Unit + ): RenderTestResult { + val action = processedAction ?: noAction() + block(action) + return this } - // Inexact matches are not consumable. - exactMatches.singleOrNull() - ?.let { expected -> - expectations -= expected - consumedExpectations += expected + override fun verifyActionResult( + block: (newState: StateT, output: WorkflowOutput?) -> Unit + ): RenderTestResult { + return verifyAction { + val (state, output) = stateAndOutput + block(state, output) } - } - - override fun requireExplicitWorkerExpectations(): - RenderTester = this.apply { - explicitWorkerExpectationsRequired = true - } - - override fun requireExplicitSideEffectExpectations(): - RenderTester = this.apply { - explicitSideEffectExpectationsRequired = true - } - - override fun send(value: WorkflowAction) { - checkNoOutputs() - check(processedAction == null) { - "Tried to send action to sink after another action was already processed:\n" + - " processed action=$processedAction\n" + - " attempted action=$value" } - processedAction = value - } - override fun verifyAction( - block: (WorkflowAction) -> Unit - ): RenderTestResult { - val action = processedAction ?: noAction() - block(action) - return this - } - - override fun verifyActionResult( - block: (newState: StateT, output: WorkflowOutput?) -> Unit - ): RenderTestResult { - return verifyAction { - val (state, output) = stateAndOutput - block(state, output) - } - } - - override fun testNextRender(): RenderTester = - testNextRenderWithProps(props) - - override fun testNextRenderWithProps( - newProps: PropsT - ): RenderTester { - val (stateAfterRender, _) = stateAndOutput - val newState = if (props != newProps) { - workflow.onPropsChanged(props, newProps, stateAfterRender) - } else { - stateAfterRender + override fun testNextRender(): RenderTester = + testNextRenderWithProps(props) + + override fun testNextRenderWithProps( + newProps: PropsT + ): RenderTester { + val (stateAfterRender, _) = stateAndOutput + val newState = if (props != newProps) { + workflow.onPropsChanged(props, newProps, stateAfterRender) + } else { + stateAfterRender + } + return RealRenderTester(workflow, newProps, newState) } - return RealRenderTester(workflow, newProps, newState) - } - private fun deepCloneForRender(): BaseRenderContext = RealRenderTester( - workflow, - props, - state, - // Copy the list of expectations since it's mutable. - expectations = ArrayList(expectations), - // Don't care about consumed expectations. - childWillEmitOutput = childWillEmitOutput, - processedAction = processedAction - ) - - private fun checkNoOutputs(newExpectation: Expectation<*>? = null) { - check(!childWillEmitOutput) { - val expectationsWithOutputs = (expectations + listOfNotNull(newExpectation)) - .filter { it.output != null } - "Expected only one child to emit an output:\n" + - expectationsWithOutputs.joinToString(separator = "\n") { " $it" } + private fun deepCloneForRender(): BaseRenderContext = RealRenderTester( + workflow, + props, + state, + // Copy the list of expectations since it's mutable. + expectations = ArrayList(expectations), + // Don't care about consumed expectations. + childWillEmitOutput = childWillEmitOutput, + processedAction = processedAction + ) + + private fun checkNoOutputs(newExpectation: Expectation<*>? = null) { + check(!childWillEmitOutput) { + val expectationsWithOutputs = (expectations + listOfNotNull(newExpectation)) + .filter { it.output != null } + "Expected only one child to emit an output:\n" + + expectationsWithOutputs.joinToString(separator = "\n") { " $it" } + } } } -} internal fun createRenderChildInvocation( workflow: Workflow<*, *, *>, diff --git a/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkflowTestRuntime.kt b/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkflowTestRuntime.kt index 9629ffba8..be54d0d07 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkflowTestRuntime.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkflowTestRuntime.kt @@ -53,120 +53,122 @@ import kotlin.coroutines.EmptyCoroutineContext * - [sendProps] * - Send a new [PropsT] to the root workflow. */ -public class WorkflowTestRuntime @TestOnly internal constructor( - private val props: MutableStateFlow, - private val renderingsAndSnapshotsFlow: Flow>, - private val outputs: ReceiveChannel -) { +public class WorkflowTestRuntime + @TestOnly + internal constructor( + private val props: MutableStateFlow, + private val renderingsAndSnapshotsFlow: Flow>, + private val outputs: ReceiveChannel + ) { - private val renderings = Channel(capacity = UNLIMITED) - private val snapshots = Channel(capacity = UNLIMITED) + private val renderings = Channel(capacity = UNLIMITED) + private val snapshots = Channel(capacity = UNLIMITED) - internal fun collectFromWorkflowIn(scope: CoroutineScope) { - // Handle cancellation before subscribing to flow in case the scope is cancelled already. - scope.coroutineContext[Job]!!.invokeOnCompletion { e -> - renderings.close(e) - snapshots.close(e) - } - - // We use NonCancellable so that if context is already cancelled, the operator chains below - // are still allowed to handle the exceptions from WorkflowHost streams explicitly, since they - // need to close the test channels. - val realScope = scope + NonCancellable - renderingsAndSnapshotsFlow - .onEach { (rendering, snapshot) -> - renderings.send(rendering) - snapshots.send(snapshot) + internal fun collectFromWorkflowIn(scope: CoroutineScope) { + // Handle cancellation before subscribing to flow in case the scope is cancelled already. + scope.coroutineContext[Job]!!.invokeOnCompletion { e -> + renderings.close(e) + snapshots.close(e) } - .launchIn(realScope) - } - /** - * True if the workflow has emitted a new rendering that is ready to be consumed. - */ - public val hasRendering: Boolean get() = !renderings.isEmptyOrClosed + // We use NonCancellable so that if context is already cancelled, the operator chains below + // are still allowed to handle the exceptions from WorkflowHost streams explicitly, since they + // need to close the test channels. + val realScope = scope + NonCancellable + renderingsAndSnapshotsFlow + .onEach { (rendering, snapshot) -> + renderings.send(rendering) + snapshots.send(snapshot) + } + .launchIn(realScope) + } - /** - * True if the workflow has emitted a new snapshot that is ready to be consumed. - */ - public val hasSnapshot: Boolean get() = !snapshots.isEmptyOrClosed + /** + * True if the workflow has emitted a new rendering that is ready to be consumed. + */ + public val hasRendering: Boolean get() = !renderings.isEmptyOrClosed - /** - * True if the workflow has emitted a new output that is ready to be consumed. - */ - public val hasOutput: Boolean get() = !outputs.isEmptyOrClosed + /** + * True if the workflow has emitted a new snapshot that is ready to be consumed. + */ + public val hasSnapshot: Boolean get() = !snapshots.isEmptyOrClosed - @OptIn(DelicateCoroutinesApi::class) - private val ReceiveChannel<*>.isEmptyOrClosed get() = isEmpty || isClosedForReceive + /** + * True if the workflow has emitted a new output that is ready to be consumed. + */ + public val hasOutput: Boolean get() = !outputs.isEmptyOrClosed - /** - * Sends [input] to the workflow. - */ - public fun sendProps(input: PropsT) { - props.value = input - } + @OptIn(DelicateCoroutinesApi::class) + private val ReceiveChannel<*>.isEmptyOrClosed get() = isEmpty || isClosedForReceive - /** - * Blocks until the workflow emits a rendering, then returns it. - * - * @param timeoutMs The maximum amount of time to wait for a rendering to be emitted. If null, - * [WorkflowTestRuntime.DEFAULT_TIMEOUT_MS] will be used instead. - * @param skipIntermediate If true, and the workflow has emitted multiple renderings, all but the - * most recent one will be dropped. - */ - public fun awaitNextRendering( - timeoutMs: Long? = null, - skipIntermediate: Boolean = true - ): RenderingT = renderings.receiveBlocking(timeoutMs, skipIntermediate) + /** + * Sends [input] to the workflow. + */ + public fun sendProps(input: PropsT) { + props.value = input + } + + /** + * Blocks until the workflow emits a rendering, then returns it. + * + * @param timeoutMs The maximum amount of time to wait for a rendering to be emitted. If null, + * [WorkflowTestRuntime.DEFAULT_TIMEOUT_MS] will be used instead. + * @param skipIntermediate If true, and the workflow has emitted multiple renderings, all but the + * most recent one will be dropped. + */ + public fun awaitNextRendering( + timeoutMs: Long? = null, + skipIntermediate: Boolean = true + ): RenderingT = renderings.receiveBlocking(timeoutMs, skipIntermediate) - /** - * Blocks until the workflow emits a snapshot, then returns it. - * - * The returned snapshot will be the snapshot only of the root workflow. It will be null if - * `snapshotState` returned an empty [Snapshot]. - * - * @param timeoutMs The maximum amount of time to wait for a snapshot to be emitted. If null, - * [DEFAULT_TIMEOUT_MS] will be used instead. - * @param skipIntermediate If true, and the workflow has emitted multiple snapshots, all but the - * most recent one will be dropped. - */ - public fun awaitNextSnapshot( - timeoutMs: Long? = null, - skipIntermediate: Boolean = true - ): TreeSnapshot = snapshots.receiveBlocking(timeoutMs, skipIntermediate) + /** + * Blocks until the workflow emits a snapshot, then returns it. + * + * The returned snapshot will be the snapshot only of the root workflow. It will be null if + * `snapshotState` returned an empty [Snapshot]. + * + * @param timeoutMs The maximum amount of time to wait for a snapshot to be emitted. If null, + * [DEFAULT_TIMEOUT_MS] will be used instead. + * @param skipIntermediate If true, and the workflow has emitted multiple snapshots, all but the + * most recent one will be dropped. + */ + public fun awaitNextSnapshot( + timeoutMs: Long? = null, + skipIntermediate: Boolean = true + ): TreeSnapshot = snapshots.receiveBlocking(timeoutMs, skipIntermediate) - /** - * Blocks until the workflow emits an output, then returns it. - * - * @param timeoutMs The maximum amount of time to wait for an output to be emitted. If null, - * [DEFAULT_TIMEOUT_MS] will be used instead. - */ - public fun awaitNextOutput(timeoutMs: Long? = null): OutputT = - outputs.receiveBlocking(timeoutMs, drain = false) + /** + * Blocks until the workflow emits an output, then returns it. + * + * @param timeoutMs The maximum amount of time to wait for an output to be emitted. If null, + * [DEFAULT_TIMEOUT_MS] will be used instead. + */ + public fun awaitNextOutput(timeoutMs: Long? = null): OutputT = + outputs.receiveBlocking(timeoutMs, drain = false) - /** - * @param drain If true, this function will consume all the values currently in the channel, and - * return the last one. - */ - private fun ReceiveChannel.receiveBlocking( - timeoutMs: Long?, - drain: Boolean - ): T = runBlocking { - withTimeout(timeoutMs ?: DEFAULT_TIMEOUT_MS) { - var item = receive() - if (drain) { - while (!isEmpty) { - item = receive() + /** + * @param drain If true, this function will consume all the values currently in the channel, and + * return the last one. + */ + private fun ReceiveChannel.receiveBlocking( + timeoutMs: Long?, + drain: Boolean + ): T = runBlocking { + withTimeout(timeoutMs ?: DEFAULT_TIMEOUT_MS) { + var item = receive() + if (drain) { + while (!isEmpty) { + item = receive() + } } + return@withTimeout item } - return@withTimeout item } - } - public companion object { - public const val DEFAULT_TIMEOUT_MS: Long = 500 + public companion object { + public const val DEFAULT_TIMEOUT_MS: Long = 500 + } } -} /** * Creates a [WorkflowTestRuntime] to run this workflow for unit testing. diff --git a/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt b/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt index 0b4c2e10c..c60981143 100644 --- a/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt +++ b/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt @@ -385,8 +385,9 @@ internal class RealRenderTesterTest { assertEquals("Expected side effect keys to be unique: \"key\"", error.message) } + @Suppress("ktlint:standard:max-line-length") @Test - fun `renderChild rendering non-Unit throws when none expected and unexpected children are allowed`() { // ktlint-disable max-line-length + fun `renderChild rendering non-Unit throws when none expected and unexpected children are allowed`() { val child = Workflow.stateless { 42 } val workflow = Workflow.stateless { renderChild(child) @@ -560,8 +561,9 @@ internal class RealRenderTesterTest { tester.render() } + @Suppress("ktlint:standard:max-line-length") @Test - fun `runningSideEffect does throw when none expected and require explicit side effect is set`() { // ktlint-disable max-line-length + fun `runningSideEffect does throw when none expected and require explicit side effect is set`() { val key = "foo" val workflow = Workflow.stateless { runningSideEffect(key = key) { } @@ -855,10 +857,10 @@ internal class RealRenderTesterTest { class TestImpostor(val proxy: Workflow<*, *, *>) : Workflow, ImpostorWorkflow { - override val realIdentifier: WorkflowIdentifier get() = proxy.identifier - override fun asStatefulWorkflow(): StatefulWorkflow = - throw NotImplementedError() - } + override val realIdentifier: WorkflowIdentifier get() = proxy.identifier + override fun asStatefulWorkflow(): StatefulWorkflow = + throw NotImplementedError() + } val workflow = Workflow.stateless { renderChild(TestImpostor(TestWorkflow())) @@ -871,8 +873,9 @@ internal class RealRenderTesterTest { .render {} } + @Suppress("ktlint:standard:max-line-length") @Test - fun `expectWorkflow doesn't match same ImpostorWorkflow class with different proxy identifiers`() { // ktlint-disable max-line-length + fun `expectWorkflow doesn't match same ImpostorWorkflow class with different proxy identifiers`() { class TestWorkflowActual : Workflow { override fun asStatefulWorkflow(): StatefulWorkflow = throw NotImplementedError() @@ -886,10 +889,10 @@ internal class RealRenderTesterTest { class TestImpostor(val proxy: Workflow<*, *, *>) : Workflow, ImpostorWorkflow { - override val realIdentifier: WorkflowIdentifier get() = proxy.identifier - override fun asStatefulWorkflow(): StatefulWorkflow = - throw NotImplementedError() - } + override val realIdentifier: WorkflowIdentifier get() = proxy.identifier + override fun asStatefulWorkflow(): StatefulWorkflow = + throw NotImplementedError() + } val workflow = Workflow.stateless { renderChild(TestImpostor(TestWorkflowActual())) @@ -919,18 +922,18 @@ internal class RealRenderTesterTest { class TestImpostorActual(val proxy: Workflow<*, *, *>) : Workflow, ImpostorWorkflow { - override val realIdentifier: WorkflowIdentifier get() = proxy.identifier - override fun asStatefulWorkflow(): StatefulWorkflow = - throw NotImplementedError() - } + override val realIdentifier: WorkflowIdentifier get() = proxy.identifier + override fun asStatefulWorkflow(): StatefulWorkflow = + throw NotImplementedError() + } class TestImpostorExpected(val proxy: Workflow<*, *, *>) : Workflow, ImpostorWorkflow { - override val realIdentifier: WorkflowIdentifier get() = proxy.identifier - override fun asStatefulWorkflow(): StatefulWorkflow = - throw NotImplementedError() - } + override val realIdentifier: WorkflowIdentifier get() = proxy.identifier + override fun asStatefulWorkflow(): StatefulWorkflow = + throw NotImplementedError() + } val workflow = Workflow.stateless { renderChild(TestImpostorActual(TestWorkflow())) @@ -1474,8 +1477,9 @@ internal class RealRenderTesterTest { assertFalse(actual.realTypeMatchesExpectation(expected)) } + @Suppress("ktlint:standard:max-line-length") @Test - fun `realTypeMatchesExpectation() doesn't match exact contravariant type with supertype parameter`() { // ktlint-disable max-line-length + fun `realTypeMatchesExpectation() doesn't match exact contravariant type with supertype parameter`() { val expected = unsnapshottableIdentifier(typeOf>()) val actual = unsnapshottableIdentifier(typeOf>()) assertFalse(actual.realTypeMatchesExpectation(expected)) diff --git a/workflow-testing/src/test/java/com/squareup/workflow1/testing/RenderIdempotencyCheckerTest.kt b/workflow-testing/src/test/java/com/squareup/workflow1/testing/RenderIdempotencyCheckerTest.kt index 82584fe64..269108f29 100644 --- a/workflow-testing/src/test/java/com/squareup/workflow1/testing/RenderIdempotencyCheckerTest.kt +++ b/workflow-testing/src/test/java/com/squareup/workflow1/testing/RenderIdempotencyCheckerTest.kt @@ -39,7 +39,8 @@ class RenderIdempotencyCheckerTest { @Test fun `events sent to sink read after render are accepted`() { val workflow = Workflow.stateless Unit> { - { value: String -> + { + value: String -> actionSink.send( action { setOutput(value) diff --git a/workflow-tracing/dependencies/runtimeClasspath.txt b/workflow-tracing/dependencies/runtimeClasspath.txt index aaeae6d22..1562a659c 100644 --- a/workflow-tracing/dependencies/runtimeClasspath.txt +++ b/workflow-tracing/dependencies/runtimeClasspath.txt @@ -2,12 +2,12 @@ com.squareup.moshi:moshi-adapters:1.15.0 com.squareup.moshi:moshi:1.15.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 -org.jetbrains.kotlin:kotlin-stdlib:1.8.21 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt index da1efde2f..2825917ff 100644 --- a/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose-tooling/dependencies/releaseRuntimeClasspath.txt @@ -45,13 +45,13 @@ androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt index f68ab6bf0..dc8cb2375 100644 --- a/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/compose/dependencies/releaseRuntimeClasspath.txt @@ -42,13 +42,13 @@ androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt index e060be47e..e8f562f72 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/RenderAsStateTest.kt @@ -89,7 +89,10 @@ internal class RenderAsStateTest { @Test fun invokesOutputCallback() { val workflow = Workflow.stateless Unit> { - { string -> actionSink.send(action { setOutput(string) }) } + { + string -> + actionSink.send(action { setOutput(string) }) + } } val receivedOutputs = mutableListOf() lateinit var rendering: (String) -> Unit diff --git a/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt index f77cb5b9d..ab2805755 100644 --- a/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/container-android/dependencies/releaseRuntimeClasspath.txt @@ -39,13 +39,13 @@ androidx.viewpager:viewpager:1.0.0 com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt index c3968d380..d96208e66 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/AlertContainer.kt @@ -28,83 +28,85 @@ import com.squareup.workflow1.ui.modal.AlertScreen.Event.Canceled */ @WorkflowUiExperimentalApi @Deprecated("Use BodyAndModalsContainer and the built in OverlayDialogFactory for AlertOverlay") -public class AlertContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0, - @StyleRes private val dialogThemeResId: Int = 0 -) : ModalContainer(context, attributeSet, defStyle, defStyleRes) { +public class AlertContainer + @JvmOverloads + constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0, + @StyleRes private val dialogThemeResId: Int = 0 + ) : ModalContainer(context, attributeSet, defStyle, defStyleRes) { - override fun buildDialog( - initialModalRendering: AlertScreen, - initialViewEnvironment: ViewEnvironment - ): DialogRef { - val dialog = AlertDialog.Builder(context, dialogThemeResId) - .create() - val ref = DialogRef(initialModalRendering, initialViewEnvironment, dialog) - updateDialog(ref) - return ref - } + override fun buildDialog( + initialModalRendering: AlertScreen, + initialViewEnvironment: ViewEnvironment + ): DialogRef { + val dialog = AlertDialog.Builder(context, dialogThemeResId) + .create() + val ref = DialogRef(initialModalRendering, initialViewEnvironment, dialog) + updateDialog(ref) + return ref + } - override fun updateDialog(dialogRef: DialogRef) { - val dialog = dialogRef.dialog as AlertDialog - val rendering = dialogRef.modalRendering + override fun updateDialog(dialogRef: DialogRef) { + val dialog = dialogRef.dialog as AlertDialog + val rendering = dialogRef.modalRendering - if (rendering.cancelable) { - dialog.setOnCancelListener { rendering.onEvent(Canceled) } - dialog.setCancelable(true) - } else { - dialog.setCancelable(false) - } + if (rendering.cancelable) { + dialog.setOnCancelListener { rendering.onEvent(Canceled) } + dialog.setCancelable(true) + } else { + dialog.setCancelable(false) + } - for (button in Button.values()) { - rendering.buttons[button] - ?.let { name -> - dialog.setButton(button.toId(), name) { _, _ -> - rendering.onEvent(ButtonClicked(button)) + for (button in Button.values()) { + rendering.buttons[button] + ?.let { name -> + dialog.setButton(button.toId(), name) { _, _ -> + rendering.onEvent(ButtonClicked(button)) + } + } + ?: run { + dialog.getButton(button.toId()) + ?.visibility = View.INVISIBLE } - } - ?: run { - dialog.getButton(button.toId()) - ?.visibility = View.INVISIBLE - } + } + + dialog.setMessage(rendering.message) + dialog.setTitle(rendering.title) } - dialog.setMessage(rendering.message) - dialog.setTitle(rendering.title) - } + private fun Button.toId(): Int = when (this) { + POSITIVE -> DialogInterface.BUTTON_POSITIVE + NEGATIVE -> DialogInterface.BUTTON_NEGATIVE + NEUTRAL -> DialogInterface.BUTTON_NEUTRAL + } - private fun Button.toId(): Int = when (this) { - POSITIVE -> DialogInterface.BUTTON_POSITIVE - NEGATIVE -> DialogInterface.BUTTON_NEGATIVE - NEUTRAL -> DialogInterface.BUTTON_NEUTRAL - } + private class AlertContainerViewFactory( + @StyleRes private val dialogThemeResId: Int = 0 + ) : ViewFactory> by BuilderViewFactory( + type = AlertContainerScreen::class, + viewConstructor = { initialRendering, initialEnv, context, _ -> + AlertContainer(context, dialogThemeResId = dialogThemeResId) + .apply { + id = R.id.workflow_alert_container + layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + bindShowRendering(initialRendering, initialEnv, ::update) + } + } + ) - private class AlertContainerViewFactory( - @StyleRes private val dialogThemeResId: Int = 0 - ) : ViewFactory> by BuilderViewFactory( - type = AlertContainerScreen::class, - viewConstructor = { initialRendering, initialEnv, context, _ -> - AlertContainer(context, dialogThemeResId = dialogThemeResId) - .apply { - id = R.id.workflow_alert_container - layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering(initialRendering, initialEnv, ::update) - } + public companion object : ViewFactory> by AlertContainerViewFactory() { + /** + * Creates a [ViewFactory] to show the [AlertScreen]s of an [AlertContainerScreen] + * as Android `AlertDialog`s. + * + * @param dialogThemeResId the resource ID of the theme against which to inflate + * dialogs. Defaults to `0` to use the parent `context`'s default alert dialog theme. + */ + public fun customThemeBinding( + @StyleRes dialogThemeResId: Int = 0 + ): ViewFactory> = AlertContainerViewFactory(dialogThemeResId) } - ) - - public companion object : ViewFactory> by AlertContainerViewFactory() { - /** - * Creates a [ViewFactory] to show the [AlertScreen]s of an [AlertContainerScreen] - * as Android `AlertDialog`s. - * - * @param dialogThemeResId the resource ID of the theme against which to inflate - * dialogs. Defaults to `0` to use the parent `context`'s default alert dialog theme. - */ - public fun customThemeBinding( - @StyleRes dialogThemeResId: Int = 0 - ): ViewFactory> = AlertContainerViewFactory(dialogThemeResId) } -} diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt index f4a8ad88c..719b3f8d4 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalContainer.kt @@ -34,276 +34,283 @@ import com.squareup.workflow1.ui.getRendering * @param ModalRenderingT the type of the nested renderings to be shown in a dialog window. */ @WorkflowUiExperimentalApi -public abstract class ModalContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { - - private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also { - addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) - } +public abstract class ModalContainer + @JvmOverloads + constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 + ) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { + + private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also { + addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) + } - private var dialogs: List> = emptyList() - - /** - * Stores the result of looking for the nearest [LifecycleOwner] that should be the parent of all - * this container's modals. Only valid after the view has been attached. - */ - private val parentLifecycleOwner by lazy(mode = LazyThreadSafetyMode.NONE) { - WorkflowLifecycleOwner.get(this) ?: error( - "Expected to find either a ViewTreeLifecycleOwner in the view tree, or for the " + - "context to be a LifecycleOwner, in $this" - ) - } + private var dialogs: List> = emptyList() - /** - * Provides a new `SavedStateRegistryOwner` for each dialog, - * which will save to the `SavedStateRegistryOwner` of this container view. - */ - private val stateRegistryAggregator = WorkflowSavedStateRegistryAggregator() - - protected fun update( - newScreen: HasModals<*, ModalRenderingT>, - viewEnvironment: ViewEnvironment - ) { - baseViewStub.update(newScreen.beneathModals, viewEnvironment) - - val newDialogs = mutableListOf>() - for ((i, modal) in newScreen.modals.withIndex()) { - newDialogs += if (i < dialogs.size && compatible(dialogs[i].modalRendering, modal)) { - dialogs[i].copy(modalRendering = modal, viewEnvironment = viewEnvironment) - .also { updateDialog(it) } - } else { - buildDialog(modal, viewEnvironment).also { ref -> - // This is the unique id we'll use to make stateRegistryAggregator put a - // SavedStateRegistryOwner on the dialog's decorView, as required by Compose. - // We put it in this sketchy lateInit field on DialogRef rather than passing - // it through abstract buildDialog method so that subclasses can't screw it up. - ref.savedStateRegistryKey = Compatible.keyFor(modal, i.toString()) - - ref.dialog.decorView?.let { dialogView -> - // Implementations of buildDialog may set their own WorkflowLifecycleOwner on the - // content view, so to avoid interfering with them we also set it here. When the views - // are attached, this will become the parent lifecycle of the one from buildDialog if - // any, and so we can use our lifecycle to destroy-on-detach the dialog hierarchy. - WorkflowLifecycleOwner.installOn( - dialogView, - onBackPressedDispatcherOwner = (ref.dialog as? OnBackPressedDispatcherOwner) - ?: viewEnvironment.onBackPressedDispatcherOwner(this), - findParentLifecycle = { parentLifecycleOwner.lifecycle } - ) - // Ensure that each dialog has its own SavedStateRegistryOwner, - // so views in each dialog layer don't clash with other layers. - stateRegistryAggregator.installChildRegistryOwnerOn( - view = dialogView, - key = ref.savedStateRegistryKey - ) - - dialogView.addOnAttachStateChangeListener( - object : OnAttachStateChangeListener { - val dismissOnDestroy = object : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) = ref.dismiss() - } - var lifecycle: Lifecycle? = null - override fun onViewAttachedToWindow(v: View) { - // Note this is a different lifecycle than the WorkflowLifecycleOwner – it will - // probably be the owning AppCompatActivity. - lifecycle = parentLifecycleOwner.lifecycle.also { - // Android makes a lot of logcat noise if it has to close the window for us. :/ - // https://github.com/square/workflow/issues/51 - it.addObserver(dismissOnDestroy) + /** + * Stores the result of looking for the nearest [LifecycleOwner] that should be the parent of all + * this container's modals. Only valid after the view has been attached. + */ + private val parentLifecycleOwner by lazy(mode = LazyThreadSafetyMode.NONE) { + WorkflowLifecycleOwner.get(this) ?: error( + "Expected to find either a ViewTreeLifecycleOwner in the view tree, or for the " + + "context to be a LifecycleOwner, in $this" + ) + } + + /** + * Provides a new `SavedStateRegistryOwner` for each dialog, + * which will save to the `SavedStateRegistryOwner` of this container view. + */ + private val stateRegistryAggregator = WorkflowSavedStateRegistryAggregator() + + protected fun update( + newScreen: HasModals<*, ModalRenderingT>, + viewEnvironment: ViewEnvironment + ) { + baseViewStub.update(newScreen.beneathModals, viewEnvironment) + + val newDialogs = mutableListOf>() + for ((i, modal) in newScreen.modals.withIndex()) { + newDialogs += if (i < dialogs.size && compatible(dialogs[i].modalRendering, modal)) { + dialogs[i].copy(modalRendering = modal, viewEnvironment = viewEnvironment) + .also { updateDialog(it) } + } else { + buildDialog(modal, viewEnvironment).also { ref -> + // This is the unique id we'll use to make stateRegistryAggregator put a + // SavedStateRegistryOwner on the dialog's decorView, as required by Compose. + // We put it in this sketchy lateInit field on DialogRef rather than passing + // it through abstract buildDialog method so that subclasses can't screw it up. + ref.savedStateRegistryKey = Compatible.keyFor(modal, i.toString()) + + ref.dialog.decorView?.let { dialogView -> + // Implementations of buildDialog may set their own WorkflowLifecycleOwner on the + // content view, so to avoid interfering with them we also set it here. When the views + // are attached, this will become the parent lifecycle of the one from buildDialog if + // any, and so we can use our lifecycle to destroy-on-detach the dialog hierarchy. + WorkflowLifecycleOwner.installOn( + dialogView, + onBackPressedDispatcherOwner = (ref.dialog as? OnBackPressedDispatcherOwner) + ?: viewEnvironment.onBackPressedDispatcherOwner(this), + findParentLifecycle = { parentLifecycleOwner.lifecycle } + ) + // Ensure that each dialog has its own SavedStateRegistryOwner, + // so views in each dialog layer don't clash with other layers. + stateRegistryAggregator.installChildRegistryOwnerOn( + view = dialogView, + key = ref.savedStateRegistryKey + ) + + dialogView.addOnAttachStateChangeListener( + object : OnAttachStateChangeListener { + val dismissOnDestroy = object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) = ref.dismiss() + } + var lifecycle: Lifecycle? = null + override fun onViewAttachedToWindow(v: View) { + // Note this is a different lifecycle than the WorkflowLifecycleOwner – it will + // probably be the owning AppCompatActivity. + lifecycle = parentLifecycleOwner.lifecycle.also { + // Android makes a lot of logcat noise if it has to close the window for us. :/ + // https://github.com/square/workflow/issues/51 + it.addObserver(dismissOnDestroy) + } } - } - override fun onViewDetachedFromWindow(v: View) { - lifecycle?.removeObserver(dismissOnDestroy) - lifecycle = null + override fun onViewDetachedFromWindow(v: View) { + lifecycle?.removeObserver(dismissOnDestroy) + lifecycle = null + } } - } - ) + ) + } + ref.dialog.show() } - ref.dialog.show() } } - } - (dialogs - newDialogs).forEach { it.dismiss() } - // Drop the state registries for any keys that no longer exist since the last save. - // Or really, drop everything except the remaining ones. - stateRegistryAggregator.pruneAllChildRegistryOwnersExcept( - keysToKeep = newDialogs.map { it.savedStateRegistryKey } - ) - dialogs = newDialogs - } + (dialogs - newDialogs).forEach { it.dismiss() } + // Drop the state registries for any keys that no longer exist since the last save. + // Or really, drop everything except the remaining ones. + stateRegistryAggregator.pruneAllChildRegistryOwnersExcept( + keysToKeep = newDialogs.map { it.savedStateRegistryKey } + ) + dialogs = newDialogs + } - /** - * Called to create (but not show) a Dialog to render [initialModalRendering]. - */ - protected abstract fun buildDialog( - initialModalRendering: ModalRenderingT, - initialViewEnvironment: ViewEnvironment - ): DialogRef - - protected abstract fun updateDialog(dialogRef: DialogRef) - - override fun onSaveInstanceState(): Parcelable { - return SavedState( - super.onSaveInstanceState()!!, - dialogs.map { it.save() } - ) - } + /** + * Called to create (but not show) a Dialog to render [initialModalRendering]. + */ + protected abstract fun buildDialog( + initialModalRendering: ModalRenderingT, + initialViewEnvironment: ViewEnvironment + ): DialogRef + + protected abstract fun updateDialog(dialogRef: DialogRef) + + override fun onSaveInstanceState(): Parcelable { + return SavedState( + super.onSaveInstanceState()!!, + dialogs.map { it.save() } + ) + } - override fun onRestoreInstanceState(state: Parcelable) { - (state as? SavedState) - ?.let { - if (it.dialogBundles.size == dialogs.size) { - it.dialogBundles.zip(dialogs) { viewState, dialogRef -> dialogRef.restore(viewState) } + override fun onRestoreInstanceState(state: Parcelable) { + (state as? SavedState) + ?.let { + if (it.dialogBundles.size == dialogs.size) { + it.dialogBundles.zip(dialogs) { viewState, dialogRef -> dialogRef.restore(viewState) } + } + super.onRestoreInstanceState(state.superState) } - super.onRestoreInstanceState(state.superState) - } - ?: super.onRestoreInstanceState(super.onSaveInstanceState()) - // ?: Some other class wrote state, but we're not allowed to skip - // the call to super. Make a no-op call. - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - val parentRegistry = stateRegistryOwnerFromViewTreeOrContext(this) - val key = Compatible.keyFor(this.getRendering()!!) - stateRegistryAggregator.attachToParentRegistry(key, parentRegistry) - } - - override fun onDetachedFromWindow() { - stateRegistryAggregator.detachFromParentRegistry() - super.onDetachedFromWindow() - } + ?: super.onRestoreInstanceState(super.onSaveInstanceState()) + // ?: Some other class wrote state, but we're not allowed to skip + // the call to super. Make a no-op call. + } - internal data class KeyAndBundle( - val compatibilityKey: String, - val bundle: Bundle - ) : Parcelable { - override fun describeContents(): Int = 0 + override fun onAttachedToWindow() { + super.onAttachedToWindow() + val parentRegistry = stateRegistryOwnerFromViewTreeOrContext(this) + val key = Compatible.keyFor(this.getRendering()!!) + stateRegistryAggregator.attachToParentRegistry(key, parentRegistry) + } - override fun writeToParcel( - parcel: Parcel, - flags: Int - ) { - parcel.writeString(compatibilityKey) - parcel.writeBundle(bundle) + override fun onDetachedFromWindow() { + stateRegistryAggregator.detachFromParentRegistry() + super.onDetachedFromWindow() } - companion object CREATOR : Creator { - override fun createFromParcel(parcel: Parcel): KeyAndBundle { - val key = parcel.readString()!! - val bundle = parcel.readBundle(KeyAndBundle::class.java.classLoader)!! - return KeyAndBundle(key, bundle) + internal data class KeyAndBundle( + val compatibilityKey: String, + val bundle: Bundle + ) : Parcelable { + override fun describeContents(): Int = 0 + + override fun writeToParcel( + parcel: Parcel, + flags: Int + ) { + parcel.writeString(compatibilityKey) + parcel.writeBundle(bundle) } - override fun newArray(size: Int): Array = arrayOfNulls(size) + companion object CREATOR : Creator { + override fun createFromParcel(parcel: Parcel): KeyAndBundle { + val key = parcel.readString()!! + val bundle = parcel.readBundle(KeyAndBundle::class.java.classLoader)!! + return KeyAndBundle(key, bundle) + } + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } } - } - /** - * @param extra optional hook to allow subclasses to associate extra data with this dialog, - * e.g. its content view. Not considered for equality. - */ - @WorkflowUiExperimentalApi - protected class DialogRef( - public val modalRendering: ModalRenderingT, - public val viewEnvironment: ViewEnvironment, - public val dialog: Dialog, - public val extra: Any? = null - ) { /** - * The unique id of the `SavedStateRegistryOwner` that will be placed - * on the dialog's decor view by [stateRegistryAggregator]. + * @param extra optional hook to allow subclasses to associate extra data with this dialog, + * e.g. its content view. Not considered for equality. */ - internal lateinit var savedStateRegistryKey: String - - public fun copy( - modalRendering: ModalRenderingT = this.modalRendering, - viewEnvironment: ViewEnvironment = this.viewEnvironment, - dialog: Dialog = this.dialog, - extra: Any? = this.extra - ): DialogRef = DialogRef(modalRendering, viewEnvironment, dialog, extra).also { - it.savedStateRegistryKey = savedStateRegistryKey - } + @WorkflowUiExperimentalApi + protected class DialogRef( + public val modalRendering: ModalRenderingT, + public val viewEnvironment: ViewEnvironment, + public val dialog: Dialog, + public val extra: Any? = null + ) { + /** + * The unique id of the `SavedStateRegistryOwner` that will be placed + * on the dialog's decor view by [stateRegistryAggregator]. + */ + internal lateinit var savedStateRegistryKey: String + + public fun copy( + modalRendering: ModalRenderingT = this.modalRendering, + viewEnvironment: ViewEnvironment = this.viewEnvironment, + dialog: Dialog = this.dialog, + extra: Any? = this.extra + ): DialogRef = DialogRef( + modalRendering, + viewEnvironment, + dialog, + extra + ).also { + it.savedStateRegistryKey = savedStateRegistryKey + } - internal fun save(): KeyAndBundle { - val saved = dialog.window!!.saveHierarchyState() - return KeyAndBundle(Compatible.keyFor(modalRendering), saved) - } + internal fun save(): KeyAndBundle { + val saved = dialog.window!!.saveHierarchyState() + return KeyAndBundle(Compatible.keyFor(modalRendering), saved) + } - internal fun restore(keyAndBundle: KeyAndBundle) { - if (Compatible.keyFor(modalRendering) == keyAndBundle.compatibilityKey) { - dialog.window!!.restoreHierarchyState(keyAndBundle.bundle) + internal fun restore(keyAndBundle: KeyAndBundle) { + if (Compatible.keyFor(modalRendering) == keyAndBundle.compatibilityKey) { + dialog.window!!.restoreHierarchyState(keyAndBundle.bundle) + } } - } - /** - * Call this instead of calling `dialog.dismiss()` directly – this method ensures that the modal's - * [WorkflowLifecycleOwner] is destroyed correctly. - */ - internal fun dismiss() { - // The dialog's views are about to be detached, and when that happens we want to transition - // the dialog view's lifecycle to a terminal state even though the parent is probably still - // alive. - dialog.decorView?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() - dialog.dismiss() - } + /** + * Call this instead of calling `dialog.dismiss()` directly – this method ensures that the modal's + * [WorkflowLifecycleOwner] is destroyed correctly. + */ + internal fun dismiss() { + // The dialog's views are about to be detached, and when that happens we want to transition + // the dialog view's lifecycle to a terminal state even though the parent is probably still + // alive. + dialog.decorView?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() + dialog.dismiss() + } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false - other as DialogRef<*> + other as DialogRef<*> - if (dialog != other.dialog) return false + if (dialog != other.dialog) return false - return true - } + return true + } - override fun hashCode(): Int { - return dialog.hashCode() + override fun hashCode(): Int { + return dialog.hashCode() + } } - } - private class SavedState : BaseSavedState { - constructor( - superState: Parcelable?, - dialogBundles: List - ) : super(superState) { - this.dialogBundles = dialogBundles - } + private class SavedState : BaseSavedState { + constructor( + superState: Parcelable?, + dialogBundles: List + ) : super(superState) { + this.dialogBundles = dialogBundles + } - constructor(source: Parcel) : super(source) { - @Suppress("UNCHECKED_CAST") - dialogBundles = mutableListOf().apply { - source.readTypedList(this, KeyAndBundle) + constructor(source: Parcel) : super(source) { + @Suppress("UNCHECKED_CAST") + dialogBundles = mutableListOf().apply { + source.readTypedList(this, KeyAndBundle) + } } - } - val dialogBundles: List + val dialogBundles: List - override fun writeToParcel( - out: Parcel, - flags: Int - ) { - super.writeToParcel(out, flags) - @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") - out.writeTypedList(dialogBundles) - } + override fun writeToParcel( + out: Parcel, + flags: Int + ) { + super.writeToParcel(out, flags) + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + out.writeTypedList(dialogBundles) + } - companion object CREATOR : Creator { - override fun createFromParcel(source: Parcel): SavedState = - SavedState(source) + companion object CREATOR : Creator { + override fun createFromParcel(source: Parcel): SavedState = + SavedState(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } } } -} private val Dialog.decorView: View? get() = window?.decorView diff --git a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt index 223b7a12e..0ff1d3ddb 100644 --- a/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt +++ b/workflow-ui/container-android/src/main/java/com/squareup/workflow1/ui/modal/ModalViewContainer.kt @@ -38,131 +38,133 @@ import kotlin.reflect.KClass * Use [binding] to assign particular rendering types to be shown this way. */ @WorkflowUiExperimentalApi -public open class ModalViewContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : ModalContainer(context, attributeSet, defStyle, defStyleRes) { +public open class ModalViewContainer + @JvmOverloads + constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 + ) : ModalContainer(context, attributeSet, defStyle, defStyleRes) { - /** - * Called from [buildDialog]. Builds (but does not show) the [Dialog] to - * display a [view] built via [ViewRegistry]. - * - * Subclasses may override completely to build their own kind of [Dialog], - * there is no need to call `super`. - */ - public open fun buildDialogForView(view: View): Dialog { - return Dialog(context).apply { - setCancelable(false) - setContentView(view) + /** + * Called from [buildDialog]. Builds (but does not show) the [Dialog] to + * display a [view] built via [ViewRegistry]. + * + * Subclasses may override completely to build their own kind of [Dialog], + * there is no need to call `super`. + */ + public open fun buildDialogForView(view: View): Dialog { + return Dialog(context).apply { + setCancelable(false) + setContentView(view) - // Dialog is sized to wrap the view. Note that this call must come after - // setContentView. - window!!.setLayout(WRAP_CONTENT, WRAP_CONTENT) + // Dialog is sized to wrap the view. Note that this call must come after + // setContentView. + window!!.setLayout(WRAP_CONTENT, WRAP_CONTENT) - // If we don't set or clear the background drawable, the window cannot go full bleed. - window!!.setBackgroundDrawable(null) + // If we don't set or clear the background drawable, the window cannot go full bleed. + window!!.setBackgroundDrawable(null) + } } - } - final override fun buildDialog( - initialModalRendering: Any, - initialViewEnvironment: ViewEnvironment - ): DialogRef { - // Put a no-op backPressedHandler behind the given rendering, to - // ensure that the `onBackPressed` call below will not leak up to handlers - // that should be blocked by this modal session. - val wrappedRendering = BackButtonScreen(asScreen(initialModalRendering)) { } + final override fun buildDialog( + initialModalRendering: Any, + initialViewEnvironment: ViewEnvironment + ): DialogRef { + // Put a no-op backPressedHandler behind the given rendering, to + // ensure that the `onBackPressed` call below will not leak up to handlers + // that should be blocked by this modal session. + val wrappedRendering = BackButtonScreen(asScreen(initialModalRendering)) { } - val viewHolder = wrappedRendering.toViewFactory(initialViewEnvironment) - .startShowing( - initialRendering = wrappedRendering, - initialEnvironment = initialViewEnvironment, - contextForNewView = this.context, - container = this - ) { view, doStart -> - // Note that we never call destroyOnDetach for this owner. That's okay because - // ModalContainer.update puts one in place above us on the decor view, - // and cleans it up. It's in place by the time we attach to the window, and - // so becomes our parent. - WorkflowLifecycleOwner.installOn( - view, - initialViewEnvironment.onBackPressedDispatcherOwner(view) - ) - doStart() - } + val viewHolder = wrappedRendering.toViewFactory(initialViewEnvironment) + .startShowing( + initialRendering = wrappedRendering, + initialEnvironment = initialViewEnvironment, + contextForNewView = this.context, + container = this + ) { view, doStart -> + // Note that we never call destroyOnDetach for this owner. That's okay because + // ModalContainer.update puts one in place above us on the decor view, + // and cleans it up. It's in place by the time we attach to the window, and + // so becomes our parent. + WorkflowLifecycleOwner.installOn( + view, + initialViewEnvironment.onBackPressedDispatcherOwner(view) + ) + doStart() + } - return buildDialogForView(viewHolder.view) - .apply { - // Dialogs are modal windows and so they block events, including back button presses - // -- that's their job! But we *want* the Activity's onBackPressedDispatcher to fire - // when back is pressed, so long as it doesn't look past this modal window for handlers. - // - // Here, we handle the ACTION_UP portion of a KEYCODE_BACK key event, and below - // we make sure that the root view has a backPressedHandler that will consume the - // onBackPressed call if no child of the root modal view does. + return buildDialogForView(viewHolder.view) + .apply { + // Dialogs are modal windows and so they block events, including back button presses + // -- that's their job! But we *want* the Activity's onBackPressedDispatcher to fire + // when back is pressed, so long as it doesn't look past this modal window for handlers. + // + // Here, we handle the ACTION_UP portion of a KEYCODE_BACK key event, and below + // we make sure that the root view has a backPressedHandler that will consume the + // onBackPressed call if no child of the root modal view does. - setOnKeyListener { _, keyCode, keyEvent -> - if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == ACTION_UP) { - viewHolder.view.onBackPressedDispatcherOwnerOrNull() - ?.onBackPressedDispatcher - ?.let { - if (it.hasEnabledCallbacks()) it.onBackPressed() - } - true - } else { - false + setOnKeyListener { _, keyCode, keyEvent -> + if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == ACTION_UP) { + viewHolder.view.onBackPressedDispatcherOwnerOrNull() + ?.onBackPressedDispatcher + ?.let { + if (it.hasEnabledCallbacks()) it.onBackPressed() + } + true + } else { + false + } } } - } - .run { - DialogRef(initialModalRendering, initialViewEnvironment, this, viewHolder) - } - } - - override fun updateDialog(dialogRef: DialogRef) { - with(dialogRef) { - // Have to preserve the wrapping done in buildDialog. (We can't put the - // BackButtonScreen in the DialogRef because the superclass needs to be - // able to do compatibility checks against it when deciding whether - // or not to update the existing dialog.) - val wrappedRendering = BackButtonScreen(asScreen(modalRendering)) { } - @Suppress("UNCHECKED_CAST") - (extra as ScreenViewHolder).show(wrappedRendering, viewEnvironment) + .run { + DialogRef(initialModalRendering, initialViewEnvironment, this, viewHolder) + } } - } - @PublishedApi - internal class ModalViewFactory>( - @IdRes id: Int, - type: KClass - ) : com.squareup.workflow1.ui.ViewFactory - by BuilderViewFactory( - type = type, - viewConstructor = { initialRendering, initialEnv, context, _ -> - ModalViewContainer(context).apply { - this.id = id - layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) - bindShowRendering(initialRendering, initialEnv, ::update) + override fun updateDialog(dialogRef: DialogRef) { + with(dialogRef) { + // Have to preserve the wrapping done in buildDialog. (We can't put the + // BackButtonScreen in the DialogRef because the superclass needs to be + // able to do compatibility checks against it when deciding whether + // or not to update the existing dialog.) + val wrappedRendering = BackButtonScreen(asScreen(modalRendering)) { } + @Suppress("UNCHECKED_CAST") + (extra as ScreenViewHolder).show(wrappedRendering, viewEnvironment) } } - ) - public companion object { - /** - * Creates a [ViewFactory][com.squareup.workflow1.ui.ViewFactory] for - * modal container screens of type [H]. - * - * Each view created for [HasModals.modals] will be shown in a [Dialog] - * whose window is set to size itself to `WRAP_CONTENT` (see [android.view.Window.setLayout]). - * - * @param id a unique identifier for containers of this type, allowing them to participate - * view persistence - */ - public inline fun > binding( - @IdRes id: Int = View.NO_ID - ): com.squareup.workflow1.ui.ViewFactory = - ModalViewFactory(id, H::class) + @PublishedApi + internal class ModalViewFactory>( + @IdRes id: Int, + type: KClass + ) : com.squareup.workflow1.ui.ViewFactory + by BuilderViewFactory( + type = type, + viewConstructor = { initialRendering, initialEnv, context, _ -> + ModalViewContainer(context).apply { + this.id = id + layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + bindShowRendering(initialRendering, initialEnv, ::update) + } + } + ) + + public companion object { + /** + * Creates a [ViewFactory][com.squareup.workflow1.ui.ViewFactory] for + * modal container screens of type [H]. + * + * Each view created for [HasModals.modals] will be shown in a [Dialog] + * whose window is set to size itself to `WRAP_CONTENT` (see [android.view.Window.setLayout]). + * + * @param id a unique identifier for containers of this type, allowing them to participate + * view persistence + */ + public inline fun > binding( + @IdRes id: Int = View.NO_ID + ): com.squareup.workflow1.ui.ViewFactory = + ModalViewFactory(id, H::class) + } } -} diff --git a/workflow-ui/container-common/api/container-common.api b/workflow-ui/container-common/api/container-common.api index 5998033f2..3aeb87fed 100644 --- a/workflow-ui/container-common/api/container-common.api +++ b/workflow-ui/container-common/api/container-common.api @@ -59,6 +59,7 @@ public final class com/squareup/workflow1/ui/modal/AlertScreen$Button : java/lan public static final field NEGATIVE Lcom/squareup/workflow1/ui/modal/AlertScreen$Button; public static final field NEUTRAL Lcom/squareup/workflow1/ui/modal/AlertScreen$Button; public static final field POSITIVE Lcom/squareup/workflow1/ui/modal/AlertScreen$Button; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/ui/modal/AlertScreen$Button; public static fun values ()[Lcom/squareup/workflow1/ui/modal/AlertScreen$Button; } diff --git a/workflow-ui/container-common/dependencies/runtimeClasspath.txt b/workflow-ui/container-common/dependencies/runtimeClasspath.txt index c0a590ef4..d8cb7353d 100644 --- a/workflow-ui/container-common/dependencies/runtimeClasspath.txt +++ b/workflow-ui/container-common/dependencies/runtimeClasspath.txt @@ -1,11 +1,11 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt index ee0ffdad2..011ff0ff6 100644 --- a/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/core-android/dependencies/releaseRuntimeClasspath.txt @@ -24,13 +24,13 @@ androidx.versionedparcelable:versionedparcelable:1.1.1 com.google.guava:listenablefuture:1.0 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt index 452635715..30a36d9c3 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt @@ -36,22 +36,22 @@ internal class DialogIntegrationTest { private data class ContentRendering(val name: String) : Compatible, AndroidScreen { - override val compatibilityKey = name - override val viewFactory: ScreenViewFactory - get() = ScreenViewFactory.fromCode { _, initialRendering, context, _ -> - ScreenViewHolder( - initialRendering, - EditText(context).apply { - layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - // Must have an id to participate in view persistence. - id = 65 - // Give us something to search for so that we can be sure - // views actually get displayed - text = SpannableStringBuilder(name) - } - ) { _, _ -> } - } - } + override val compatibilityKey = name + override val viewFactory: ScreenViewFactory + get() = ScreenViewFactory.fromCode { _, initialRendering, context, _ -> + ScreenViewHolder( + initialRendering, + EditText(context).apply { + layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) + // Must have an id to participate in view persistence. + id = 65 + // Give us something to search for so that we can be sure + // views actually get displayed + text = SpannableStringBuilder(name) + } + ) { _, _ -> } + } + } private var latestDialog: Dialog? = null diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt index 26e0b8060..cd7f72800 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/WorkflowViewStub.kt @@ -64,208 +64,212 @@ import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner * for animated transitions. */ @WorkflowUiExperimentalApi -public class WorkflowViewStub @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : View(context, attributeSet, defStyle, defStyleRes) { - private var holder: ScreenViewHolder? = null +public class WorkflowViewStub + @JvmOverloads + constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 + ) : View(context, attributeSet, defStyle, defStyleRes) { + private var holder: ScreenViewHolder? = null - /** - * On-demand access to the view created by the last call to [update], - * or this [WorkflowViewStub] instance if none has yet been made. - */ - public val actual: View get() = holder?.view ?: this + /** + * On-demand access to the view created by the last call to [update], + * or this [WorkflowViewStub] instance if none has yet been made. + */ + public val actual: View get() = holder?.view ?: this - /** - * If true, the visibility of views created by [update] will be copied - * from that of [actual]. Bear in mind that the initial value of - * [actual] is this stub. - */ - public var updatesVisibility: Boolean = true + /** + * If true, the visibility of views created by [update] will be copied + * from that of [actual]. Bear in mind that the initial value of + * [actual] is this stub. + */ + public var updatesVisibility: Boolean = true - /** - * The id to be assigned to new views created by [update]. If the inflated id is - * [View.NO_ID] (its default value), new views keep their original ids. - */ - @IdRes public var inflatedId: Int = NO_ID - set(value) { - require(value == NO_ID || value != id) { - "inflatedId must be distinct from id: ${resources.getResourceName(id)}" + /** + * The id to be assigned to new views created by [update]. If the inflated id is + * [View.NO_ID] (its default value), new views keep their original ids. + */ + @IdRes public var inflatedId: Int = NO_ID + set(value) { + require(value == NO_ID || value != id) { + "inflatedId must be distinct from id: ${resources.getResourceName(id)}" + } + field = value } - field = value - } - - init { - val attrs = context.obtainStyledAttributes( - attributeSet, - R.styleable.WorkflowViewStub, - defStyle, - defStyleRes - ) - inflatedId = attrs.getResourceId(R.styleable.WorkflowViewStub_inflatedId, NO_ID) - updatesVisibility = attrs.getBoolean(R.styleable.WorkflowViewStub_updatesVisibility, true) - attrs.recycle() - setWillNotDraw(true) - } + init { + val attrs = context.obtainStyledAttributes( + attributeSet, + R.styleable.WorkflowViewStub, + defStyle, + defStyleRes + ) + inflatedId = attrs.getResourceId(R.styleable.WorkflowViewStub_inflatedId, NO_ID) + updatesVisibility = attrs.getBoolean(R.styleable.WorkflowViewStub_updatesVisibility, true) + attrs.recycle() - override fun setId(@IdRes id: Int) { - require(id == NO_ID || id != inflatedId) { - "id must be distinct from inflatedId: ${resources.getResourceName(id)}" + setWillNotDraw(true) } - super.setId(id) - } - - /** - * Function called from [update] to replace this stub, or the current [actual], - * with a new view. Can be updated to provide custom transition effects. - * - * Note that this method is responsible for copying the [layoutParams][getLayoutParams] - * from the stub to the new view. Also note that in a [WorkflowViewStub] that has never - * been updated, [actual] is the stub itself. - */ - public var replaceOldViewInParent: (ViewGroup, View) -> Unit = { parent, newView -> - val index = parent.indexOfChild(actual) - parent.removeView(actual) - actual.layoutParams - ?.let { parent.addView(newView, index, it) } - ?: run { parent.addView(newView, index) } - } - /** - * Sets the visibility of [actual]. If [updatesVisibility] is true, the visibility of - * new views created by [update] will copied from [actual]. (Bear in mind that the initial - * value of [actual] is this stub.) - */ - override fun setVisibility(visibility: Int) { - super.setVisibility(visibility) - // actual can be null when called from the constructor. - @Suppress("SENSELESS_COMPARISON") - if (actual != this && actual != null) { - actual.visibility = visibility + override fun setId( + @IdRes id: Int + ) { + require(id == NO_ID || id != inflatedId) { + "id must be distinct from inflatedId: ${resources.getResourceName(id)}" + } + super.setId(id) } - } - /** - * Returns the visibility of [actual]. (Bear in mind that the initial value of - * [actual] is this stub.) - */ - override fun getVisibility(): Int { - // actual can be null when called from the constructor. - @Suppress("SENSELESS_NULL_IN_WHEN") - return when (actual) { - this, null -> super.getVisibility() - else -> actual.visibility + /** + * Function called from [update] to replace this stub, or the current [actual], + * with a new view. Can be updated to provide custom transition effects. + * + * Note that this method is responsible for copying the [layoutParams][getLayoutParams] + * from the stub to the new view. Also note that in a [WorkflowViewStub] that has never + * been updated, [actual] is the stub itself. + */ + public var replaceOldViewInParent: (ViewGroup, View) -> Unit = { parent, newView -> + val index = parent.indexOfChild(actual) + parent.removeView(actual) + actual.layoutParams + ?.let { parent.addView(newView, index, it) } + ?: run { parent.addView(newView, index) } } - } - /** - * Sets the background of this stub as usual, and also that of [actual] - * if the given [background] is not null. Any new views created by [update] - * will be assigned this background, again if it is not null. - */ - override fun setBackground(background: Drawable?) { - super.setBackground(background) - // actual can be null when called from the constructor. - @Suppress("SENSELESS_COMPARISON") - if (actual != this && actual != null && background != null) { - actual.background = background + /** + * Sets the visibility of [actual]. If [updatesVisibility] is true, the visibility of + * new views created by [update] will copied from [actual]. (Bear in mind that the initial + * value of [actual] is this stub.) + */ + override fun setVisibility(visibility: Int) { + super.setVisibility(visibility) + // actual can be null when called from the constructor. + @Suppress("SENSELESS_COMPARISON") + if (actual != this && actual != null) { + actual.visibility = visibility + } } - } - @Deprecated( - "Use show()", - ReplaceWith( - "show(asScreen(rendering), viewEnvironment)", - "com.squareup.workflow1.ui.asScreen" - ), - ) - public fun update( - rendering: Any, - viewEnvironment: ViewEnvironment - ): View { - show(asScreen(rendering), viewEnvironment) - return holder!!.view - } - - /** - * Replaces this view with one that can display [rendering]. If the receiver - * has already been replaced, updates the replacement if it [canShowRendering]. - * If the current replacement can't handle [rendering], a new view is put in its place. - * - * The [id][View.setId] of any view created by this method will be set to to [inflatedId], - * unless that value is [View.NO_ID]. - * - * The [background][setBackground] of any view created by this method will be copied - * from [getBackground], if that value is non-null. - * - * If [updatesVisibility] is true, the [visibility][setVisibility] of any view created by - * this method will be copied from [actual]. (Bear in mind that the initial value of - * [actual] is this stub.) - * - * @return the view that showed [rendering] - * - * @throws IllegalArgumentException if no binding can be found for the type of [rendering] - */ - public fun show( - rendering: Screen, - viewEnvironment: ViewEnvironment - ) { - holder?.takeIf { it.canShow(rendering) } - ?.let { - it.show(rendering, viewEnvironment) - return + /** + * Returns the visibility of [actual]. (Bear in mind that the initial value of + * [actual] is this stub.) + */ + override fun getVisibility(): Int { + // actual can be null when called from the constructor. + @Suppress("SENSELESS_NULL_IN_WHEN") + return when (actual) { + this, null -> super.getVisibility() + else -> actual.visibility } + } - val parent = actual.parent as? ViewGroup - ?: throw IllegalStateException("WorkflowViewStub must have a non-null ViewGroup parent") + /** + * Sets the background of this stub as usual, and also that of [actual] + * if the given [background] is not null. Any new views created by [update] + * will be assigned this background, again if it is not null. + */ + override fun setBackground(background: Drawable?) { + super.setBackground(background) + // actual can be null when called from the constructor. + @Suppress("SENSELESS_COMPARISON") + if (actual != this && actual != null && background != null) { + actual.background = background + } + } - holder?.view?.let { - // The old view is about to be detached by replaceOldViewInParent. When that happens, - // it's not just a regular detach, it's a navigation event that effectively says that view - // will never come back. Thus, we want its Lifecycle to move to permanently destroyed, even - // though the parent lifecycle is still probably alive. - WorkflowLifecycleOwner.get(it)?.destroyOnDetach() + @Deprecated( + "Use show()", + ReplaceWith( + "show(asScreen(rendering), viewEnvironment)", + "com.squareup.workflow1.ui.asScreen" + ), + ) + public fun update( + rendering: Any, + viewEnvironment: ViewEnvironment + ): View { + show(asScreen(rendering), viewEnvironment) + return holder!!.view } - holder = rendering.toViewFactory(viewEnvironment) - .startShowing(rendering, viewEnvironment, parent.context, parent) { view, doStart -> - WorkflowLifecycleOwner.installOn(view, viewEnvironment.onBackPressedDispatcherOwner(parent)) - doStart() - }.apply { - val newView = view + /** + * Replaces this view with one that can display [rendering]. If the receiver + * has already been replaced, updates the replacement if it [canShowRendering]. + * If the current replacement can't handle [rendering], a new view is put in its place. + * + * The [id][View.setId] of any view created by this method will be set to to [inflatedId], + * unless that value is [View.NO_ID]. + * + * The [background][setBackground] of any view created by this method will be copied + * from [getBackground], if that value is non-null. + * + * If [updatesVisibility] is true, the [visibility][setVisibility] of any view created by + * this method will be copied from [actual]. (Bear in mind that the initial value of + * [actual] is this stub.) + * + * @return the view that showed [rendering] + * + * @throws IllegalArgumentException if no binding can be found for the type of [rendering] + */ + public fun show( + rendering: Screen, + viewEnvironment: ViewEnvironment + ) { + holder?.takeIf { it.canShow(rendering) } + ?.let { + it.show(rendering, viewEnvironment) + return + } - if (inflatedId != NO_ID) newView.id = inflatedId - if (updatesVisibility) newView.visibility = visibility - background?.let { newView.background = it } - propagateSavedStateRegistryOwner(newView) - replaceOldViewInParent(parent, newView) + val parent = actual.parent as? ViewGroup + ?: throw IllegalStateException("WorkflowViewStub must have a non-null ViewGroup parent") + + holder?.view?.let { + // The old view is about to be detached by replaceOldViewInParent. When that happens, + // it's not just a regular detach, it's a navigation event that effectively says that view + // will never come back. Thus, we want its Lifecycle to move to permanently destroyed, even + // though the parent lifecycle is still probably alive. + WorkflowLifecycleOwner.get(it)?.destroyOnDetach() } - } - /** - * If a [SavedStateRegistryOwner] was set on this [WorkflowViewStub], sets that owner on - * [newView]. Note that this _only_ copies an owner if it was set _directly_ on this view with - * [setViewTreeSavedStateRegistryOwner]. If [findViewTreeSavedStateRegistryOwner] would return an - * owner that was set on a parent view, this method does nothing. - * - * Must be called before [newView] gets attached to the window. - */ - private fun propagateSavedStateRegistryOwner(newView: View) { - // There's no way to ask for the owner only on this view, without looking up the tree, so - // we have to compare the results from searching from this view to searching from our parent - // (if we have a parent) to determine if we have our own owner. - val myStateRegistryOwner = this.findViewTreeSavedStateRegistryOwner() - val parentStateRegistryOwner = - (this.parent as? ViewGroup)?.run { - findViewTreeSavedStateRegistryOwner() + holder = rendering.toViewFactory(viewEnvironment) + .startShowing(rendering, viewEnvironment, parent.context, parent) { view, doStart -> + WorkflowLifecycleOwner.installOn(view, viewEnvironment.onBackPressedDispatcherOwner(parent)) + doStart() + }.apply { + val newView = view + + if (inflatedId != NO_ID) newView.id = inflatedId + if (updatesVisibility) newView.visibility = visibility + background?.let { newView.background = it } + propagateSavedStateRegistryOwner(newView) + replaceOldViewInParent(parent, newView) + } + } + + /** + * If a [SavedStateRegistryOwner] was set on this [WorkflowViewStub], sets that owner on + * [newView]. Note that this _only_ copies an owner if it was set _directly_ on this view with + * [setViewTreeSavedStateRegistryOwner]. If [findViewTreeSavedStateRegistryOwner] would return an + * owner that was set on a parent view, this method does nothing. + * + * Must be called before [newView] gets attached to the window. + */ + private fun propagateSavedStateRegistryOwner(newView: View) { + // There's no way to ask for the owner only on this view, without looking up the tree, so + // we have to compare the results from searching from this view to searching from our parent + // (if we have a parent) to determine if we have our own owner. + val myStateRegistryOwner = this.findViewTreeSavedStateRegistryOwner() + val parentStateRegistryOwner = + (this.parent as? ViewGroup)?.run { + findViewTreeSavedStateRegistryOwner() + } + if (myStateRegistryOwner !== parentStateRegistryOwner) { + // Someone has set an owner on the stub itself, so we need to also set it on the new + // subview. + newView.setViewTreeSavedStateRegistryOwner(myStateRegistryOwner) } - if (myStateRegistryOwner !== parentStateRegistryOwner) { - // Someone has set an owner on the stub itself, so we need to also set it on the new - // subview. - newView.setViewTreeSavedStateRegistryOwner(myStateRegistryOwner) } } -} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt index 3f3f29cc7..a7937c48e 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowLifecycleOwner.kt @@ -124,163 +124,163 @@ internal class RealWorkflowLifecycleOwner( OnAttachStateChangeListener, LifecycleEventObserver { - private var view: View? = null - set(value) { - field = value - // This is only called non-null from onViewAttachedToWindow, so the logic is sound. - viewIsAttachedToWindow = value != null - } + private var view: View? = null + set(value) { + field = value + // This is only called non-null from onViewAttachedToWindow, so the logic is sound. + viewIsAttachedToWindow = value != null + } - /** - * We track this state ourselves rather than relying on view.isAttachedToWindow - * because that call returns true until after all onDetachedFromWindow calls - * are made. - * - * Mainly set as a side effect of setting [view], except that we set it to false - * eagerly from [onViewDetachedFromWindow]. - */ - private var viewIsAttachedToWindow: Boolean = false + /** + * We track this state ourselves rather than relying on view.isAttachedToWindow + * because that call returns true until after all onDetachedFromWindow calls + * are made. + * + * Mainly set as a side effect of setting [view], except that we set it to false + * eagerly from [onViewDetachedFromWindow]. + */ + private var viewIsAttachedToWindow: Boolean = false - private val localLifecycle = - if (enforceMainThread) LifecycleRegistry(this) else createUnsafe(this) + private val localLifecycle = + if (enforceMainThread) LifecycleRegistry(this) else createUnsafe(this) - override val lifecycle: Lifecycle - get() = localLifecycle + override val lifecycle: Lifecycle + get() = localLifecycle - /** - * We can't rely on [localLifecycle] to know if we've actually attempted to destroy the lifecycle, - * because there's a case where we can be about to destroy our lifecycle but we're still in the - * INITIALIZED state so we just stay there (because that's an invalid transition). This flag lets - * us determine if we've done that and should skip work in onAttached. - * - * Maybe this means that [destroyOnDetach] is too eager, and should actually wait for _attach_ and - * _then_ detach? - */ - private var hasBeenDestroyed = false + /** + * We can't rely on [localLifecycle] to know if we've actually attempted to destroy the lifecycle, + * because there's a case where we can be about to destroy our lifecycle but we're still in the + * INITIALIZED state so we just stay there (because that's an invalid transition). This flag lets + * us determine if we've done that and should skip work in onAttached. + * + * Maybe this means that [destroyOnDetach] is too eager, and should actually wait for _attach_ and + * _then_ detach? + */ + private var hasBeenDestroyed = false - /** - * The parent lifecycle found by calling [findViewTreeLifecycleOwner] on the owning view's parent - * (once it's attached), or if no [LifecycleOwner] is set, then by trying to find a - * [LifecycleOwner] on the view's context. - * - * When the view is detached, we keep the reference to the previous parent - * lifecycle, and keep observing it, to ensure we get destroyed correctly if the parent is - * destroyed while we're detached. The next time we're attached, we search for a parent again, in - * case we're attached in a different subtree that has a different parent. - * - * This is only null in two cases: - * 1. The view hasn't been attached yet, ever. - * 2. The lifecycle has been destroyed. - */ - private var parentLifecycle: Lifecycle? = null - private var destroyOnDetach = false + /** + * The parent lifecycle found by calling [findViewTreeLifecycleOwner] on the owning view's parent + * (once it's attached), or if no [LifecycleOwner] is set, then by trying to find a + * [LifecycleOwner] on the view's context. + * + * When the view is detached, we keep the reference to the previous parent + * lifecycle, and keep observing it, to ensure we get destroyed correctly if the parent is + * destroyed while we're detached. The next time we're attached, we search for a parent again, in + * case we're attached in a different subtree that has a different parent. + * + * This is only null in two cases: + * 1. The view hasn't been attached yet, ever. + * 2. The lifecycle has been destroyed. + */ + private var parentLifecycle: Lifecycle? = null + private var destroyOnDetach = false - override fun onViewAttachedToWindow(v: View) { - if (localLifecycle.currentState == DESTROYED || hasBeenDestroyed) { - return - } + override fun onViewAttachedToWindow(v: View) { + if (localLifecycle.currentState == DESTROYED || hasBeenDestroyed) { + return + } - this.view = v + this.view = v - // Always check for a new parent, in case we're attached to different part of the view tree. - val oldLifecycle = parentLifecycle - parentLifecycle = findParentLifecycle(v) + // Always check for a new parent, in case we're attached to different part of the view tree. + val oldLifecycle = parentLifecycle + parentLifecycle = findParentLifecycle(v) - if (parentLifecycle !== oldLifecycle) { - oldLifecycle?.removeObserver(this) - parentLifecycle?.addObserver(this) + if (parentLifecycle !== oldLifecycle) { + oldLifecycle?.removeObserver(this) + parentLifecycle?.addObserver(this) + } + updateLifecycle() } - updateLifecycle() - } - override fun onViewDetachedFromWindow(v: View) { - viewIsAttachedToWindow = false - updateLifecycle() - } - - /** Called when the [parentLifecycle] changes state. */ - override fun onStateChanged( - source: LifecycleOwner, - event: Event - ) { - updateLifecycle() - } - - override fun destroyOnDetach() { - if (!destroyOnDetach) { - destroyOnDetach = true + override fun onViewDetachedFromWindow(v: View) { + viewIsAttachedToWindow = false updateLifecycle() } - } - - @VisibleForTesting(otherwise = PRIVATE) - internal fun updateLifecycle() { - val parentState = parentLifecycle?.currentState - val localState = localLifecycle.currentState - if (localState == DESTROYED || hasBeenDestroyed) { - view = null - // Local lifecycle is in a terminal state. - return + /** Called when the [parentLifecycle] changes state. */ + override fun onStateChanged( + source: LifecycleOwner, + event: Event + ) { + updateLifecycle() } - localLifecycle.currentState = when { - destroyOnDetach && !viewIsAttachedToWindow -> { - // We've been enqueued for destruction. - // Stay attached to the parent's lifecycle until we re-attach, since the parent could be - // destroyed while we're detached. - DESTROYED - } - parentState != null -> { - // We may or may not be attached, but we have a parent lifecycle so we just blindly follow - // it. - parentState - } - localState == INITIALIZED -> { - // We have no parent and we're not destroyed, which means we have never been attached, so - // the only valid state we can be in is INITIALIZED. - INITIALIZED + override fun destroyOnDetach() { + if (!destroyOnDetach) { + destroyOnDetach = true + updateLifecycle() } - else -> { - // We don't have a parent and we're neither in DESTROYED or INITIALIZED: this is an invalid - // state. Throw an AssertionError instead of IllegalStateException because there's no API to - // get into this state, so this means the library has a bug. - throw AssertionError( - "Must have a parent lifecycle after attaching and until being destroyed." - ) - } - }.let { newState -> - if (newState == DESTROYED) { - hasBeenDestroyed = true + } - // We just transitioned to a terminal DESTROY state. Be a good citizen and make sure to - // detach from our parent. - // - // Note that if localState is INITIALIZED, this is not a valid transition and - // LifecycleRegistry will throw when we try setting currentState. This is not a situation - // that it should be possible to get in unless there's a bug in this library, which is why - // we don't explicitly check for it. - parentLifecycle?.removeObserver(this) - parentLifecycle = null + @VisibleForTesting(otherwise = PRIVATE) + internal fun updateLifecycle() { + val parentState = parentLifecycle?.currentState + val localState = localLifecycle.currentState - // We can't change state anymore, so we don't care about watching for new parents. - view?.let { - view = null - it.removeOnAttachStateChangeListener(this) - } + if (localState == DESTROYED || hasBeenDestroyed) { + view = null + // Local lifecycle is in a terminal state. + return + } - // In tests, a test failure can cause us to destroy the lifecycle before it's been moved - // out of the INITIALIZED state. That's an invalid state transition, and so setCurrentState - // will throw if we do that. That exception can mask actual test failures, so to avoid that - // here we just stay in the initialized state forever. - if (localState == INITIALIZED) { + localLifecycle.currentState = when { + destroyOnDetach && !viewIsAttachedToWindow -> { + // We've been enqueued for destruction. + // Stay attached to the parent's lifecycle until we re-attach, since the parent could be + // destroyed while we're detached. + DESTROYED + } + parentState != null -> { + // We may or may not be attached, but we have a parent lifecycle so we just blindly follow + // it. + parentState + } + localState == INITIALIZED -> { + // We have no parent and we're not destroyed, which means we have never been attached, so + // the only valid state we can be in is INITIALIZED. INITIALIZED + } + else -> { + // We don't have a parent and we're neither in DESTROYED or INITIALIZED: this is an invalid + // state. Throw an AssertionError instead of IllegalStateException because there's no API to + // get into this state, so this means the library has a bug. + throw AssertionError( + "Must have a parent lifecycle after attaching and until being destroyed." + ) + } + }.let { newState -> + if (newState == DESTROYED) { + hasBeenDestroyed = true + + // We just transitioned to a terminal DESTROY state. Be a good citizen and make sure to + // detach from our parent. + // + // Note that if localState is INITIALIZED, this is not a valid transition and + // LifecycleRegistry will throw when we try setting currentState. This is not a situation + // that it should be possible to get in unless there's a bug in this library, which is why + // we don't explicitly check for it. + parentLifecycle?.removeObserver(this) + parentLifecycle = null + + // We can't change state anymore, so we don't care about watching for new parents. + view?.let { + view = null + it.removeOnAttachStateChangeListener(this) + } + + // In tests, a test failure can cause us to destroy the lifecycle before it's been moved + // out of the INITIALIZED state. That's an invalid state transition, and so setCurrentState + // will throw if we do that. That exception can mask actual test failures, so to avoid that + // here we just stay in the initialized state forever. + if (localState == INITIALIZED) { + INITIALIZED + } else { + DESTROYED + } } else { - DESTROYED + newState } - } else { - newState } } } -} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt index 8006d6a9a..d5716a185 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BackStackContainer.kt @@ -44,191 +44,193 @@ import com.squareup.workflow1.ui.toViewFactory * 2. AndroidX [SavedStateRegistry] via [SavedStateRegistryOwner]. */ @WorkflowUiExperimentalApi -public open class BackStackContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { - - private val viewStateCache = ViewStateCache() - - private var currentViewHolder: ScreenViewHolder>? = null - private var currentRendering: BackStackScreen>? = null - - /** - * Unique identifier for this view for SavedStateRegistry purposes. Based on the - * [Compatible.keyFor] the current rendering. Taking this approach allows - * feature developers to take control over naming, e.g. by wrapping renderings - * with [NamedScreen][com.squareup.workflow1.ui.NamedScreen]. - */ - private lateinit var savedStateParentKey: String - - public fun update( - newRendering: BackStackScreen<*>, - newViewEnvironment: ViewEnvironment - ) { - savedStateParentKey = keyFor(screen) - - val config = if (newRendering.backStack.isEmpty()) First else Other - val environment = newViewEnvironment + config - - val named: BackStackScreen> = newRendering - // ViewStateCache requires that everything be Named. - // It's fine if client code is already using Named for its own purposes, recursion works. - .map { NamedScreen(it, "backstack") } - - val oldViewHolderMaybe = currentViewHolder - - // If existing view is compatible, just update it. - oldViewHolderMaybe - ?.takeIf { it.canShow(named.top) } - ?.let { - viewStateCache.prune(named.frames) - it.show(named.top, environment) - return - } - - val newViewHolder = named.top.toViewFactory(environment).startShowing( - initialRendering = named.top, - initialEnvironment = environment, - contextForNewView = this.context, - container = this, - viewStarter = { view, doStart -> - WorkflowLifecycleOwner.installOn(view, environment.onBackPressedDispatcherOwner(this)) - doStart() - } - ) - viewStateCache.update(named.backStack, oldViewHolderMaybe, newViewHolder) +public open class BackStackContainer + @JvmOverloads + constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 + ) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { + + private val viewStateCache = ViewStateCache() + + private var currentViewHolder: ScreenViewHolder>? = null + private var currentRendering: BackStackScreen>? = null + + /** + * Unique identifier for this view for SavedStateRegistry purposes. Based on the + * [Compatible.keyFor] the current rendering. Taking this approach allows + * feature developers to take control over naming, e.g. by wrapping renderings + * with [NamedScreen][com.squareup.workflow1.ui.NamedScreen]. + */ + private lateinit var savedStateParentKey: String + + public fun update( + newRendering: BackStackScreen<*>, + newViewEnvironment: ViewEnvironment + ) { + savedStateParentKey = keyFor(screen) - val popped = currentRendering?.backStack?.any { compatible(it, named.top) } == true + val config = if (newRendering.backStack.isEmpty()) First else Other + val environment = newViewEnvironment + config - performTransition(oldViewHolderMaybe, newViewHolder, popped) - // Notify the view we're about to replace that it's going away. - oldViewHolderMaybe?.view?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() + val named: BackStackScreen> = newRendering + // ViewStateCache requires that everything be Named. + // It's fine if client code is already using Named for its own purposes, recursion works. + .map { NamedScreen(it, "backstack") } - currentViewHolder = newViewHolder - currentRendering = named - } + val oldViewHolderMaybe = currentViewHolder - /** - * Called from [update] (via [ScreenViewHolder.show] to swap between views. Subclasses - * can override to customize visual effects. There is no need to call super. Note that - * views are showing renderings of type [NamedScreen]`<*>`. - * - * @param oldHolderMaybe the outgoing view, or null if this is the initial rendering. - * @param newHolder the view that should replace [oldHolderMaybe] (if it exists), and become - * this view's only child - * @param popped true if we should give the appearance of popping "back" to a previous rendering, - * false if a new rendering is being "pushed". Should be ignored if [oldHolderMaybe] is null. - */ - protected open fun performTransition( - oldHolderMaybe: ScreenViewHolder>?, - newHolder: ScreenViewHolder>, - popped: Boolean - ) { - // Showing something already, transition with push or pop effect. - oldHolderMaybe - ?.let { oldHolder -> - val oldBody: View? = oldHolder.view.findViewById(R.id.back_stack_body) - val newBody: View? = newHolder.view.findViewById(R.id.back_stack_body) - - val oldTarget: View - val newTarget: View - if (oldBody != null && newBody != null) { - oldTarget = oldBody - newTarget = newBody - } else { - oldTarget = oldHolder.view - newTarget = newHolder.view + // If existing view is compatible, just update it. + oldViewHolderMaybe + ?.takeIf { it.canShow(named.top) } + ?.let { + viewStateCache.prune(named.frames) + it.show(named.top, environment) + return } - val (outEdge, inEdge) = when (popped) { - false -> Gravity.START to Gravity.END - true -> Gravity.END to Gravity.START + val newViewHolder = named.top.toViewFactory(environment).startShowing( + initialRendering = named.top, + initialEnvironment = environment, + contextForNewView = this.context, + container = this, + viewStarter = { view, doStart -> + WorkflowLifecycleOwner.installOn(view, environment.onBackPressedDispatcherOwner(this)) + doStart() } + ) + viewStateCache.update(named.backStack, oldViewHolderMaybe, newViewHolder) - val transition = TransitionSet() - .addTransition(Slide(outEdge).addTarget(oldTarget)) - .addTransition(Slide(inEdge).addTarget(newTarget)) - .setInterpolator(AccelerateDecelerateInterpolator()) + val popped = currentRendering?.backStack?.any { compatible(it, named.top) } == true - TransitionManager.endTransitions(this) - TransitionManager.go(Scene(this, newHolder.view), transition) - return - } + performTransition(oldViewHolderMaybe, newViewHolder, popped) + // Notify the view we're about to replace that it's going away. + oldViewHolderMaybe?.view?.let(WorkflowLifecycleOwner::get)?.destroyOnDetach() - // This is the first view, just show it. - addView(newHolder.view) - } + currentViewHolder = newViewHolder + currentRendering = named + } + + /** + * Called from [update] (via [ScreenViewHolder.show] to swap between views. Subclasses + * can override to customize visual effects. There is no need to call super. Note that + * views are showing renderings of type [NamedScreen]`<*>`. + * + * @param oldHolderMaybe the outgoing view, or null if this is the initial rendering. + * @param newHolder the view that should replace [oldHolderMaybe] (if it exists), and become + * this view's only child + * @param popped true if we should give the appearance of popping "back" to a previous rendering, + * false if a new rendering is being "pushed". Should be ignored if [oldHolderMaybe] is null. + */ + protected open fun performTransition( + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder>, + popped: Boolean + ) { + // Showing something already, transition with push or pop effect. + oldHolderMaybe + ?.let { oldHolder -> + val oldBody: View? = oldHolder.view.findViewById(R.id.back_stack_body) + val newBody: View? = newHolder.view.findViewById(R.id.back_stack_body) + + val oldTarget: View + val newTarget: View + if (oldBody != null && newBody != null) { + oldTarget = oldBody + newTarget = newBody + } else { + oldTarget = oldHolder.view + newTarget = newHolder.view + } + + val (outEdge, inEdge) = when (popped) { + false -> Gravity.START to Gravity.END + true -> Gravity.END to Gravity.START + } + + val transition = TransitionSet() + .addTransition(Slide(outEdge).addTarget(oldTarget)) + .addTransition(Slide(inEdge).addTarget(newTarget)) + .setInterpolator(AccelerateDecelerateInterpolator()) + + TransitionManager.endTransitions(this) + TransitionManager.go(Scene(this, newHolder.view), transition) + return + } - override fun onSaveInstanceState(): Parcelable? { - return super.onSaveInstanceState()?.let { - SavedState(it, viewStateCache.save()) + // This is the first view, just show it. + addView(newHolder.view) } - } - override fun onRestoreInstanceState(state: Parcelable) { - (state as? SavedState) - ?.let { - viewStateCache.restore(it.savedViewState) - super.onRestoreInstanceState(state.superState) + override fun onSaveInstanceState(): Parcelable? { + return super.onSaveInstanceState()?.let { + SavedState(it, viewStateCache.save()) } - ?: super.onRestoreInstanceState(super.onSaveInstanceState()) - // Some other class wrote state, but we're not allowed to skip - // the call to super. Make a no-op call. - } + } - override fun onAttachedToWindow() { - super.onAttachedToWindow() + override fun onRestoreInstanceState(state: Parcelable) { + (state as? SavedState) + ?.let { + viewStateCache.restore(it.savedViewState) + super.onRestoreInstanceState(state.superState) + } + ?: super.onRestoreInstanceState(super.onSaveInstanceState()) + // Some other class wrote state, but we're not allowed to skip + // the call to super. Make a no-op call. + } - // Wire up our viewStateCache to our parent SavedStateRegistry. - val parentRegistryOwner = stateRegistryOwnerFromViewTreeOrContext(this) - viewStateCache.attachToParentRegistryOwner(savedStateParentKey, parentRegistryOwner) - } + override fun onAttachedToWindow() { + super.onAttachedToWindow() - override fun onDetachedFromWindow() { - // Disconnect our state cache from our parent SavedStateRegistry so that it doesn't get asked - // to save state anymore. - viewStateCache.detachFromParentRegistry() - super.onDetachedFromWindow() - } + // Wire up our viewStateCache to our parent SavedStateRegistry. + val parentRegistryOwner = stateRegistryOwnerFromViewTreeOrContext(this) + viewStateCache.attachToParentRegistryOwner(savedStateParentKey, parentRegistryOwner) + } - public class SavedState : BaseSavedState { - public constructor( - superState: Parcelable, - savedViewState: ViewStateCache.Saved - ) : super(superState) { - this.savedViewState = savedViewState + override fun onDetachedFromWindow() { + // Disconnect our state cache from our parent SavedStateRegistry so that it doesn't get asked + // to save state anymore. + viewStateCache.detachFromParentRegistry() + super.onDetachedFromWindow() } - public constructor(source: Parcel) : super(source) { - savedViewState = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - source.readParcelable( - ViewStateCache.Saved::class.java.classLoader, - ViewStateCache.Saved::class.java - )!! - } else { - @Suppress("DEPRECATION") - source.readParcelable(ViewStateCache.Saved::class.java.classLoader)!! + public class SavedState : BaseSavedState { + public constructor( + superState: Parcelable, + savedViewState: ViewStateCache.Saved + ) : super(superState) { + this.savedViewState = savedViewState + } + + public constructor(source: Parcel) : super(source) { + savedViewState = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + source.readParcelable( + ViewStateCache.Saved::class.java.classLoader, + ViewStateCache.Saved::class.java + )!! + } else { + @Suppress("DEPRECATION") + source.readParcelable(ViewStateCache.Saved::class.java.classLoader)!! + } } - } - public val savedViewState: ViewStateCache.Saved + public val savedViewState: ViewStateCache.Saved - override fun writeToParcel( - out: Parcel, - flags: Int - ) { - super.writeToParcel(out, flags) - out.writeParcelable(savedViewState, flags) - } + override fun writeToParcel( + out: Parcel, + flags: Int + ) { + super.writeToParcel(out, flags) + out.writeParcelable(savedViewState, flags) + } - public companion object CREATOR : Creator { - override fun createFromParcel(source: Parcel): SavedState = - SavedState(source) + public companion object CREATOR : Creator { + override fun createFromParcel(source: Parcel): SavedState = + SavedState(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } } } -} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt index ea5ef4c41..07a26da05 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt @@ -29,133 +29,135 @@ import com.squareup.workflow1.ui.screen * the need arise. */ @WorkflowUiExperimentalApi -internal class BodyAndOverlaysContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { - /** - * The unique `SavedStateRegistry` key passed to [LayeredDialogSessions.onAttachedToWindow], - * derived from the first rendering passed to [update]. See the doc on - * [LayeredDialogSessions.onAttachedToWindow] for details. - */ - private lateinit var savedStateParentKey: String - - private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also { - addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) - } +internal class BodyAndOverlaysContainer + @JvmOverloads + constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 + ) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { + /** + * The unique `SavedStateRegistry` key passed to [LayeredDialogSessions.onAttachedToWindow], + * derived from the first rendering passed to [update]. See the doc on + * [LayeredDialogSessions.onAttachedToWindow] for details. + */ + private lateinit var savedStateParentKey: String + + private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also { + addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) + } - private val dialogs = LayeredDialogSessions.forView( - view = this, - superDispatchTouchEvent = { super.dispatchTouchEvent(it) } - ) + private val dialogs = LayeredDialogSessions.forView( + view = this, + superDispatchTouchEvent = { super.dispatchTouchEvent(it) } + ) - fun update( - newScreen: BodyAndOverlaysScreen<*, *>, - viewEnvironment: ViewEnvironment - ) { - savedStateParentKey = Compatible.keyFor(screen) + fun update( + newScreen: BodyAndOverlaysScreen<*, *>, + viewEnvironment: ViewEnvironment + ) { + savedStateParentKey = Compatible.keyFor(screen) - dialogs.update(newScreen.overlays, viewEnvironment) { env -> - baseViewStub.show(newScreen.body, env) + dialogs.update(newScreen.overlays, viewEnvironment) { env -> + baseViewStub.show(newScreen.body, env) + } } - } - - override fun onAttachedToWindow() { - // I tried to move this to the attachStateChangeListener in LayeredDialogs.Companion.forView, - // but that fires too late and we crash with the dreaded - // "You can consumeRestoredStateForKey only after super.onCreate of corresponding component". - super.onAttachedToWindow() - // Wire up dialogs to our parent SavedStateRegistry. - dialogs.onAttachedToWindow(savedStateParentKey, this) - } + override fun onAttachedToWindow() { + // I tried to move this to the attachStateChangeListener in LayeredDialogs.Companion.forView, + // but that fires too late and we crash with the dreaded + // "You can consumeRestoredStateForKey only after super.onCreate of corresponding component". - override fun onDetachedFromWindow() { - // Disconnect dialogs from our parent SavedStateRegistry so that it doesn't get asked - // to save state anymore. - dialogs.onDetachedFromWindow() + super.onAttachedToWindow() + // Wire up dialogs to our parent SavedStateRegistry. + dialogs.onAttachedToWindow(savedStateParentKey, this) + } - super.onDetachedFromWindow() - } + override fun onDetachedFromWindow() { + // Disconnect dialogs from our parent SavedStateRegistry so that it doesn't get asked + // to save state anymore. + dialogs.onDetachedFromWindow() - override fun dispatchTouchEvent(event: MotionEvent): Boolean { - return !dialogs.allowBodyEvents || super.dispatchTouchEvent(event) - } + super.onDetachedFromWindow() + } - override fun dispatchKeyEvent(event: KeyEvent): Boolean { - return !dialogs.allowBodyEvents || super.dispatchKeyEvent(event) - } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return !dialogs.allowBodyEvents || super.dispatchTouchEvent(event) + } - override fun onSaveInstanceState(): Parcelable { - return SavedState( - superState = super.onSaveInstanceState()!!, - savedDialogSessions = dialogs.onSaveInstanceState() - ) - } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + return !dialogs.allowBodyEvents || super.dispatchKeyEvent(event) + } - override fun onRestoreInstanceState(state: Parcelable) { - (state as? SavedState) - ?.let { - dialogs.onRestoreInstanceState(state.savedDialogSessions) - super.onRestoreInstanceState(state.superState) - } - ?: super.onRestoreInstanceState(super.onSaveInstanceState()) - // Some other class wrote state, but we're not allowed to skip - // the call to super. Make a no-op call. - } + override fun onSaveInstanceState(): Parcelable { + return SavedState( + superState = super.onSaveInstanceState()!!, + savedDialogSessions = dialogs.onSaveInstanceState() + ) + } - private class SavedState : BaseSavedState { - constructor( - superState: Parcelable, - savedDialogSessions: LayeredDialogSessions.SavedState - ) : super(superState) { - this.savedDialogSessions = savedDialogSessions + override fun onRestoreInstanceState(state: Parcelable) { + (state as? SavedState) + ?.let { + dialogs.onRestoreInstanceState(state.savedDialogSessions) + super.onRestoreInstanceState(state.superState) + } + ?: super.onRestoreInstanceState(super.onSaveInstanceState()) + // Some other class wrote state, but we're not allowed to skip + // the call to super. Make a no-op call. } - constructor(source: Parcel) : super(source) { - savedDialogSessions = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - source.readParcelable( - LayeredDialogSessions.SavedState::class.java.classLoader, - LayeredDialogSessions.SavedState::class.java - )!! - } else { - @Suppress("DEPRECATION") - source.readParcelable(LayeredDialogSessions.SavedState::class.java.classLoader)!! + private class SavedState : BaseSavedState { + constructor( + superState: Parcelable, + savedDialogSessions: LayeredDialogSessions.SavedState + ) : super(superState) { + this.savedDialogSessions = savedDialogSessions } - } - val savedDialogSessions: LayeredDialogSessions.SavedState + constructor(source: Parcel) : super(source) { + savedDialogSessions = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + source.readParcelable( + LayeredDialogSessions.SavedState::class.java.classLoader, + LayeredDialogSessions.SavedState::class.java + )!! + } else { + @Suppress("DEPRECATION") + source.readParcelable(LayeredDialogSessions.SavedState::class.java.classLoader)!! + } + } - override fun writeToParcel( - out: Parcel, - flags: Int - ) { - super.writeToParcel(out, flags) - out.writeParcelable(savedDialogSessions, flags) - } + val savedDialogSessions: LayeredDialogSessions.SavedState + + override fun writeToParcel( + out: Parcel, + flags: Int + ) { + super.writeToParcel(out, flags) + out.writeParcelable(savedDialogSessions, flags) + } - companion object CREATOR : Creator { - override fun createFromParcel(source: Parcel): SavedState = - SavedState(source) + companion object CREATOR : Creator { + override fun createFromParcel(source: Parcel): SavedState = + SavedState(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } } - } - companion object : ScreenViewFactory> - by ScreenViewFactory.fromCode( - buildView = { _, initialEnvironment, context, _ -> - BodyAndOverlaysContainer(context) - .let { view -> - view.id = R.id.workflow_body_and_modals_container - view.layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) - - ScreenViewHolder(initialEnvironment, view) { rendering, environment -> - view.update(rendering, environment) + companion object : ScreenViewFactory> + by ScreenViewFactory.fromCode( + buildView = { _, initialEnvironment, context, _ -> + BodyAndOverlaysContainer(context) + .let { view -> + view.id = R.id.workflow_body_and_modals_container + view.layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) + + ScreenViewHolder(initialEnvironment, view) { rendering, environment -> + view.update(rendering, environment) + } } - } - } - ) -} + } + ) + } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt index 0e126c693..63bbb9ec0 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ViewStateCache.kt @@ -31,164 +31,164 @@ import com.squareup.workflow1.ui.showing */ @WorkflowUiExperimentalApi public class ViewStateCache -@VisibleForTesting(otherwise = PRIVATE) -internal constructor( - @get:VisibleForTesting(otherwise = PRIVATE) - internal val viewStates: MutableMap -) { - public constructor() : this(mutableMapOf()) - - private val stateRegistryAggregator = WorkflowSavedStateRegistryAggregator() - - /** - * To be called when the set of hidden views changes but the visible view remains - * the same. Any cached view state held for renderings that are not - * [compatible][com.squareup.workflow1.ui.compatible] those in [retaining] will be dropped. - */ - public fun prune(retaining: Collection>) { - pruneAllKeysExcept(retaining.map { it.compatibilityKey }) - } + @VisibleForTesting(otherwise = PRIVATE) + internal constructor( + @get:VisibleForTesting(otherwise = PRIVATE) + internal val viewStates: MutableMap + ) { + public constructor() : this(mutableMapOf()) - private fun pruneAllKeysExcept(retaining: Collection) { - val deadKeys = viewStates.keys - retaining - viewStates -= deadKeys - stateRegistryAggregator.pruneAllChildRegistryOwnersExcept(retaining) - } + private val stateRegistryAggregator = WorkflowSavedStateRegistryAggregator() - /** - * @param retainedRenderings the renderings to be considered hidden after this update. Any - * associated view state will be retained in the cache, possibly to be restored to [newView] - * on a succeeding call to his method. Any other cached view state will be dropped. - * - * @param oldHolderMaybe the view that is being removed, if any, which is expected to be showing - * a [NamedScreen] rendering. If that rendering is - * [compatible with][com.squareup.workflow1.ui.compatible] a member of - * [retainedRenderings], its state will be [saved][View.saveHierarchyState]. - * - * @param newHolder the view that is about to be displayed, which must be showing a - * [NamedScreen] rendering. If [compatible][com.squareup.workflow1.ui.compatible] - * view state is found in the cache, it is [restored][View.restoreHierarchyState]. - * - * @return true if [newHolder] has been restored. - */ - public fun update( - retainedRenderings: Collection>, - oldHolderMaybe: ScreenViewHolder>?, - newHolder: ScreenViewHolder> - ) { - val newKey = keyFor(newHolder.showing) - val hiddenKeys = retainedRenderings.asSequence() - .map { it.compatibilityKey } - .toSet() - .apply { - require(retainedRenderings.size == size) { - "Duplicate entries not allowed in $retainedRenderings." + /** + * To be called when the set of hidden views changes but the visible view remains + * the same. Any cached view state held for renderings that are not + * [compatible][com.squareup.workflow1.ui.compatible] those in [retaining] will be dropped. + */ + public fun prune(retaining: Collection>) { + pruneAllKeysExcept(retaining.map { it.compatibilityKey }) + } + + private fun pruneAllKeysExcept(retaining: Collection) { + val deadKeys = viewStates.keys - retaining + viewStates -= deadKeys + stateRegistryAggregator.pruneAllChildRegistryOwnersExcept(retaining) + } + + /** + * @param retainedRenderings the renderings to be considered hidden after this update. Any + * associated view state will be retained in the cache, possibly to be restored to [newView] + * on a succeeding call to his method. Any other cached view state will be dropped. + * + * @param oldHolderMaybe the view that is being removed, if any, which is expected to be showing + * a [NamedScreen] rendering. If that rendering is + * [compatible with][com.squareup.workflow1.ui.compatible] a member of + * [retainedRenderings], its state will be [saved][View.saveHierarchyState]. + * + * @param newHolder the view that is about to be displayed, which must be showing a + * [NamedScreen] rendering. If [compatible][com.squareup.workflow1.ui.compatible] + * view state is found in the cache, it is [restored][View.restoreHierarchyState]. + * + * @return true if [newHolder] has been restored. + */ + public fun update( + retainedRenderings: Collection>, + oldHolderMaybe: ScreenViewHolder>?, + newHolder: ScreenViewHolder> + ) { + val newKey = keyFor(newHolder.showing) + val hiddenKeys = retainedRenderings.asSequence() + .map { it.compatibilityKey } + .toSet() + .apply { + require(retainedRenderings.size == size) { + "Duplicate entries not allowed in $retainedRenderings." + } } - } - // Put the [SavedStateRegistryOwner] in place. - stateRegistryAggregator.installChildRegistryOwnerOn(newHolder.view, newKey) + // Put the [SavedStateRegistryOwner] in place. + stateRegistryAggregator.installChildRegistryOwnerOn(newHolder.view, newKey) + + viewStates.remove(newKey) + ?.let { newHolder.view.restoreHierarchyState(it.viewState) } - viewStates.remove(newKey) - ?.let { newHolder.view.restoreHierarchyState(it.viewState) } + // Save both the view state and state registry of the view that's going away, as long as it's + // still in the backstack. + if (oldHolderMaybe != null) { + keyFor(oldHolderMaybe.showing).takeIf { hiddenKeys.contains(it) } + ?.let { savedKey -> + // View state + val saved = SparseArray().apply { + oldHolderMaybe.view.saveHierarchyState(this) + } + viewStates += savedKey to ViewStateFrame(savedKey, saved) - // Save both the view state and state registry of the view that's going away, as long as it's - // still in the backstack. - if (oldHolderMaybe != null) { - keyFor(oldHolderMaybe.showing).takeIf { hiddenKeys.contains(it) } - ?.let { savedKey -> - // View state - val saved = SparseArray().apply { - oldHolderMaybe.view.saveHierarchyState(this) + stateRegistryAggregator.saveAndPruneChildRegistryOwner(savedKey) } - viewStates += savedKey to ViewStateFrame(savedKey, saved) + } - stateRegistryAggregator.saveAndPruneChildRegistryOwner(savedKey) - } + pruneAllKeysExcept(retaining = hiddenKeys + newKey) } - pruneAllKeysExcept(retaining = hiddenKeys + newKey) - } - - /** - * Must be called whenever the owning view is [attached to a window][View.onAttachedToWindow]. - * Must eventually be matched with a call to [detachFromParentRegistry]. - */ - public fun attachToParentRegistryOwner( - key: String, - parentOwner: SavedStateRegistryOwner - ) { - stateRegistryAggregator.attachToParentRegistry(key, parentOwner) - } - - /** - * Must be called whenever the owning view is [detached from a window][View.onDetachedFromWindow]. - * Must be matched with a call to [attachToParentRegistryOwner]. - */ - public fun detachFromParentRegistry() { - stateRegistryAggregator.detachFromParentRegistry() - } + /** + * Must be called whenever the owning view is [attached to a window][View.onAttachedToWindow]. + * Must eventually be matched with a call to [detachFromParentRegistry]. + */ + public fun attachToParentRegistryOwner( + key: String, + parentOwner: SavedStateRegistryOwner + ) { + stateRegistryAggregator.attachToParentRegistry(key, parentOwner) + } - /** - * Replaces the state of the receiver with that of [from]. Typical usage is to call this from - * a container view's [View.onRestoreInstanceState]. - */ - public fun restore(from: Saved) { - viewStates.clear() - viewStates += from.viewStates - } + /** + * Must be called whenever the owning view is [detached from a window][View.onDetachedFromWindow]. + * Must be matched with a call to [attachToParentRegistryOwner]. + */ + public fun detachFromParentRegistry() { + stateRegistryAggregator.detachFromParentRegistry() + } - /** - * Returns a [Parcelable] copy of the internal state of the receiver, for use with - * a container view's [View.onSaveInstanceState]. - */ - public fun save(): Saved { - return Saved(this) - } + /** + * Replaces the state of the receiver with that of [from]. Typical usage is to call this from + * a container view's [View.onRestoreInstanceState]. + */ + public fun restore(from: Saved) { + viewStates.clear() + viewStates += from.viewStates + } - public class Saved : Parcelable { - internal constructor(viewStateCache: ViewStateCache) { - this.viewStates = viewStateCache.viewStates.toMap() + /** + * Returns a [Parcelable] copy of the internal state of the receiver, for use with + * a container view's [View.onSaveInstanceState]. + */ + public fun save(): Saved { + return Saved(this) } - public constructor(source: Parcel) { - this.viewStates = mutableMapOf() - .apply { - @Suppress("UNCHECKED_CAST") - if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - source.readMap( - this as MutableMap, - ViewStateCache::class.java.classLoader, - Any::class.java, - Any::class.java - ) - } else { - @Suppress("DEPRECATION") - source.readMap( - this as MutableMap, - ViewStateCache::class.java.classLoader - ) + public class Saved : Parcelable { + internal constructor(viewStateCache: ViewStateCache) { + this.viewStates = viewStateCache.viewStates.toMap() + } + + public constructor(source: Parcel) { + this.viewStates = mutableMapOf() + .apply { + @Suppress("UNCHECKED_CAST") + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + source.readMap( + this as MutableMap, + ViewStateCache::class.java.classLoader, + Any::class.java, + Any::class.java + ) + } else { + @Suppress("DEPRECATION") + source.readMap( + this as MutableMap, + ViewStateCache::class.java.classLoader + ) + } } - } - .toMap() - } + .toMap() + } - internal val viewStates: Map + internal val viewStates: Map - override fun describeContents(): Int = 0 + override fun describeContents(): Int = 0 - override fun writeToParcel( - out: Parcel, - flags: Int - ) { - out.writeMap(viewStates) - } + override fun writeToParcel( + out: Parcel, + flags: Int + ) { + out.writeMap(viewStates) + } - public companion object CREATOR : Creator { - override fun createFromParcel(source: Parcel): Saved = - Saved(source) + public companion object CREATOR : Creator { + override fun createFromParcel(source: Parcel): Saved = + Saved(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } } } -} diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index 3709b2775..17b3865a2 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -172,6 +172,7 @@ public final class com/squareup/workflow1/ui/container/AlertOverlay$Button : jav public static final field NEGATIVE Lcom/squareup/workflow1/ui/container/AlertOverlay$Button; public static final field NEUTRAL Lcom/squareup/workflow1/ui/container/AlertOverlay$Button; public static final field POSITIVE Lcom/squareup/workflow1/ui/container/AlertOverlay$Button; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/ui/container/AlertOverlay$Button; public static fun values ()[Lcom/squareup/workflow1/ui/container/AlertOverlay$Button; } @@ -199,6 +200,7 @@ public final class com/squareup/workflow1/ui/container/BackStackConfig : java/la public static final field First Lcom/squareup/workflow1/ui/container/BackStackConfig; public static final field None Lcom/squareup/workflow1/ui/container/BackStackConfig; public static final field Other Lcom/squareup/workflow1/ui/container/BackStackConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/ui/container/BackStackConfig; public static fun values ()[Lcom/squareup/workflow1/ui/container/BackStackConfig; } diff --git a/workflow-ui/core-common/dependencies/runtimeClasspath.txt b/workflow-ui/core-common/dependencies/runtimeClasspath.txt index c0a590ef4..d8cb7353d 100644 --- a/workflow-ui/core-common/dependencies/runtimeClasspath.txt +++ b/workflow-ui/core-common/dependencies/runtimeClasspath.txt @@ -1,11 +1,11 @@ com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0 diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt index 3508f820d..ec6af06de 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/ViewEnvironment.kt @@ -12,54 +12,54 @@ import kotlin.reflect.KClass */ @WorkflowUiExperimentalApi public class ViewEnvironment -@Deprecated( - "To eliminate runtime errors this constructor will become private. " + - "Use ViewEnvironment.EMPTY and ViewEnvironment.plus" -) -constructor( - public val map: Map, Any> = emptyMap() -) { - public operator fun get(key: ViewEnvironmentKey): T = getOrNull(key) ?: key.default + @Deprecated( + "To eliminate runtime errors this constructor will become private. " + + "Use ViewEnvironment.EMPTY and ViewEnvironment.plus" + ) + constructor( + public val map: Map, Any> = emptyMap() + ) { + public operator fun get(key: ViewEnvironmentKey): T = getOrNull(key) ?: key.default - public operator fun plus(pair: Pair, T>): ViewEnvironment { - val (newKey, newValue) = pair - val newPair = getOrNull(newKey) - ?.let { oldValue -> newKey to newKey.combine(oldValue, newValue) } - ?: pair - @Suppress("DEPRECATION") - return ViewEnvironment(map + newPair) - } + public operator fun plus(pair: Pair, T>): ViewEnvironment { + val (newKey, newValue) = pair + val newPair = getOrNull(newKey) + ?.let { oldValue -> newKey to newKey.combine(oldValue, newValue) } + ?: pair + @Suppress("DEPRECATION") + return ViewEnvironment(map + newPair) + } - @Suppress("DEPRECATION") - public operator fun plus(other: ViewEnvironment): ViewEnvironment { - if (this == other) return this - if (other.map.isEmpty()) return this - if (map.isEmpty()) return other - val newMap = map.toMutableMap() - other.map.entries.forEach { (key, value) -> - @Suppress("UNCHECKED_CAST") - newMap[key] = getOrNull(key as ViewEnvironmentKey) - ?.let { oldValue -> key.combine(oldValue, value) } - ?: value + @Suppress("DEPRECATION") + public operator fun plus(other: ViewEnvironment): ViewEnvironment { + if (this == other) return this + if (other.map.isEmpty()) return this + if (map.isEmpty()) return other + val newMap = map.toMutableMap() + other.map.entries.forEach { (key, value) -> + @Suppress("UNCHECKED_CAST") + newMap[key] = getOrNull(key as ViewEnvironmentKey) + ?.let { oldValue -> key.combine(oldValue, value) } + ?: value + } + return ViewEnvironment(newMap) } - return ViewEnvironment(newMap) - } - override fun toString(): String = "ViewEnvironment($map)" + override fun toString(): String = "ViewEnvironment($map)" - override fun equals(other: Any?): Boolean = - (other as? ViewEnvironment)?.let { it.map == map } ?: false + override fun equals(other: Any?): Boolean = + (other as? ViewEnvironment)?.let { it.map == map } ?: false - override fun hashCode(): Int = map.hashCode() + override fun hashCode(): Int = map.hashCode() - @Suppress("UNCHECKED_CAST") - private fun getOrNull(key: ViewEnvironmentKey): T? = map[key] as? T + @Suppress("UNCHECKED_CAST") + private fun getOrNull(key: ViewEnvironmentKey): T? = map[key] as? T - public companion object { - @Suppress("DEPRECATION") - public val EMPTY: ViewEnvironment = ViewEnvironment() + public companion object { + @Suppress("DEPRECATION") + public val EMPTY: ViewEnvironment = ViewEnvironment() + } } -} /** * Defines a value type [T] that can be provided by a [ViewEnvironment] map, @@ -73,7 +73,9 @@ constructor( @WorkflowUiExperimentalApi public abstract class ViewEnvironmentKey() { @Deprecated("Use no args constructor", ReplaceWith("ViewEnvironmentKey()")) - public constructor(@Suppress("UNUSED_PARAMETER") type: KClass) : this() + public constructor( + @Suppress("UNUSED_PARAMETER") type: KClass + ) : this() /** * Defines the default value for this key. It is a grievous error for this value to be diff --git a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/BackStackScreenTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/BackStackScreenTest.kt index 93b33d28c..6ea143888 100644 --- a/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/BackStackScreenTest.kt +++ b/workflow-ui/core-common/src/test/java/com/squareup/workflow1/ui/container/BackStackScreenTest.kt @@ -12,7 +12,9 @@ internal class BackStackScreenTest { data class BarScreen(val value: T) : Screen @Test fun `top is last`() { - assertThat(BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3), FooScreen(4)).top).isEqualTo( + assertThat( + BackStackScreen(FooScreen(1), FooScreen(2), FooScreen(3), FooScreen(4)).top + ).isEqualTo( FooScreen(4) ) } @@ -23,7 +25,9 @@ internal class BackStackScreenTest { } @Test fun `get works`() { - assertThat(BackStackScreen(FooScreen("able"), FooScreen("baker"), FooScreen("charlie"))[1]).isEqualTo( + assertThat( + BackStackScreen(FooScreen("able"), FooScreen("baker"), FooScreen("charlie"))[1] + ).isEqualTo( FooScreen("baker") ) } diff --git a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt index 6d8c54564..f88240fcb 100644 --- a/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt +++ b/workflow-ui/radiography/dependencies/releaseRuntimeClasspath.txt @@ -26,13 +26,13 @@ com.squareup.curtains:curtains:1.2.2 com.squareup.okio:okio-jvm:3.3.0 com.squareup.okio:okio:3.3.0 com.squareup.radiography:radiography:2.4.1 -org.jetbrains.kotlin:kotlin-bom:1.8.10 -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -org.jetbrains.kotlin:kotlin-stdlib:1.8.20 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 +org.jetbrains.kotlin:kotlin-bom:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib:1.9.10 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains:annotations:23.0.0