Skip to content

Commit 724e219

Browse files
WIP Reimplemented Workers using side effects.
This fixes #82, which is the Kotlin half of square/workflow#1021. Also fixes square/workflow#1197.
1 parent e931852 commit 724e219

File tree

25 files changed

+504
-637
lines changed

25 files changed

+504
-637
lines changed

workflow-core/api/workflow-core.api

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public abstract interface class com/squareup/workflow/ImpostorWorkflow {
1717
public abstract class com/squareup/workflow/LifecycleWorker : com/squareup/workflow/Worker {
1818
public fun <init> ()V
1919
public fun doesSameWorkAs (Lcom/squareup/workflow/Worker;)Z
20+
public final fun getOutputType ()Lkotlin/reflect/KType;
2021
public fun onStarted ()V
2122
public fun onStopped ()V
2223
public final fun run ()Lkotlinx/coroutines/flow/Flow;
@@ -35,6 +36,7 @@ public final class com/squareup/workflow/RenderContext$DefaultImpls {
3536
public static fun makeActionSink (Lcom/squareup/workflow/RenderContext;)Lcom/squareup/workflow/Sink;
3637
public static fun onEvent (Lcom/squareup/workflow/RenderContext;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
3738
public static synthetic fun renderChild$default (Lcom/squareup/workflow/RenderContext;Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
39+
public static fun runningWorker (Lcom/squareup/workflow/RenderContext;Lcom/squareup/workflow/Worker;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
3840
public static synthetic fun runningWorker$default (Lcom/squareup/workflow/RenderContext;Lcom/squareup/workflow/Worker;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
3941
}
4042

@@ -103,13 +105,15 @@ public abstract class com/squareup/workflow/StatelessWorkflow : com/squareup/wor
103105
public final class com/squareup/workflow/TypedWorker : com/squareup/workflow/Worker {
104106
public fun <init> (Lkotlin/reflect/KType;Lkotlinx/coroutines/flow/Flow;)V
105107
public fun doesSameWorkAs (Lcom/squareup/workflow/Worker;)Z
108+
public fun getOutputType ()Lkotlin/reflect/KType;
106109
public fun run ()Lkotlinx/coroutines/flow/Flow;
107110
public fun toString ()Ljava/lang/String;
108111
}
109112

110113
public abstract interface class com/squareup/workflow/Worker {
111114
public static final field Companion Lcom/squareup/workflow/Worker$Companion;
112115
public abstract fun doesSameWorkAs (Lcom/squareup/workflow/Worker;)Z
116+
public abstract fun getOutputType ()Lkotlin/reflect/KType;
113117
public abstract fun run ()Lkotlinx/coroutines/flow/Flow;
114118
}
115119

@@ -122,6 +126,7 @@ public final class com/squareup/workflow/Worker$Companion {
122126

123127
public final class com/squareup/workflow/Worker$DefaultImpls {
124128
public static fun doesSameWorkAs (Lcom/squareup/workflow/Worker;Lcom/squareup/workflow/Worker;)Z
129+
public static fun getOutputType (Lcom/squareup/workflow/Worker;)Lkotlin/reflect/KType;
125130
}
126131

127132
public abstract interface class com/squareup/workflow/Workflow {

workflow-core/src/main/java/com/squareup/workflow/LifecycleWorker.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package com.squareup.workflow
2121
import kotlinx.coroutines.flow.Flow
2222
import kotlinx.coroutines.flow.flow
2323
import kotlinx.coroutines.suspendCancellableCoroutine
24+
import kotlin.reflect.KType
2425

2526
/**
2627
* [Worker] that performs some action when the worker is started and/or stopped.
@@ -30,6 +31,8 @@ import kotlinx.coroutines.suspendCancellableCoroutine
3031
*/
3132
abstract class LifecycleWorker : Worker<Nothing> {
3233

34+
final override val outputType: KType? get() = null
35+
3336
/**
3437
* Called when this worker is started. It is executed concurrently with the parent workflow –
3538
* the first render pass that starts this worker *will not* wait for this method to return, and
@@ -73,6 +76,5 @@ abstract class LifecycleWorker : Worker<Nothing> {
7376
/**
7477
* Equates [LifecycleWorker]s that have the same concrete class.
7578
*/
76-
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean =
77-
this::class == otherWorker::class
79+
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean = true
7880
}

workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ interface RenderContext<out PropsT, StateT, in OutputT> {
120120
worker: Worker<T>,
121121
key: String = "",
122122
handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
123-
)
123+
) {
124+
val workerWorkflow = WorkerWorkflow<T>(WorkerKType(worker), key)
125+
renderChild(workerWorkflow, props = worker, key = key, handler = handler)
126+
}
124127

125128
/**
126129
* Ensures [sideEffect] is running with the given [key].

workflow-core/src/main/java/com/squareup/workflow/Worker.kt

+21-12
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ import kotlin.reflect.typeOf
5252
* See the documentation on [doesSameWorkAs] for more details on how and when workers are compared
5353
* and the worker lifecycle.
5454
*
55+
* Implementations of this interface that are themselves parameterized on a type should override
56+
* [outputType] to return the [KType] of their type parameter. This allows the runtime to compare
57+
* workers by the output type as well as the concrete type. If [outputType] returns null, all
58+
* workers of the same concrete type will be considered equivalent.
59+
*
5560
* ## Example: Network request
5661
*
5762
* Let's say you have a network service with an API that returns a number, and you want to
@@ -117,6 +122,14 @@ import kotlin.reflect.typeOf
117122
*/
118123
interface Worker<out OutputT> {
119124

125+
/**
126+
* Should be overridden in subclasses that have their own type parameters, to allow the runtime to
127+
* make use of the value of those type parameters to compare workers. Two workers of the same
128+
* concrete [Worker] class will be considered equivalent if and only if their [outputType]s are
129+
* also equivalent. If this property returns null, the worker will be treated as a `Worker<*>`.
130+
*/
131+
val outputType: KType? get() = null
132+
120133
/**
121134
* Returns a [Flow] to execute the work represented by this worker.
122135
*
@@ -165,8 +178,7 @@ interface Worker<out OutputT> {
165178
* that performs a network request might check that two workers are requests to the same endpoint
166179
* and have the same request data.
167180
*
168-
* Most implementations of this method will check for concrete type equality, and then match
169-
* on constructor parameters.
181+
* Most implementations of this method should compare constructor parameters.
170182
*
171183
* E.g:
172184
*
@@ -179,7 +191,7 @@ interface Worker<out OutputT> {
179191
* }
180192
* ```
181193
*/
182-
fun doesSameWorkAs(otherWorker: Worker<*>): Boolean = otherWorker::class == this::class
194+
fun doesSameWorkAs(otherWorker: Worker<*>): Boolean = true
183195

184196
companion object {
185197

@@ -384,23 +396,23 @@ fun <T, R> Worker<T>.transform(
384396

385397
/**
386398
* A generic [Worker] implementation that defines equivalent workers as those having equivalent
387-
* [type]s. This is used by all the [Worker] builder functions.
399+
* [outputType]s. This is used by all the [Worker] builder functions.
388400
*/
389401
@PublishedApi
390402
internal class TypedWorker<OutputT>(
391-
private val type: KType,
403+
override val outputType: KType,
392404
private val work: Flow<OutputT>
393405
) : Worker<OutputT> {
394406

395407
override fun run(): Flow<OutputT> = work
396408

397409
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean =
398-
otherWorker is TypedWorker && otherWorker.type == type
410+
otherWorker is TypedWorker && otherWorker.outputType == outputType
399411

400-
override fun toString(): String = "TypedWorker($type)"
412+
override fun toString(): String = "TypedWorker($outputType)"
401413
}
402414

403-
private class TimerWorker(
415+
private data class TimerWorker(
404416
private val delayMs: Long,
405417
private val key: String
406418
) : Worker<Unit> {
@@ -412,17 +424,14 @@ private class TimerWorker(
412424

413425
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean =
414426
otherWorker is TimerWorker && otherWorker.key == key
415-
416-
override fun toString(): String = "TimerWorker(delayMs=$delayMs)"
417427
}
418428

419429
private object FinishedWorker : Worker<Nothing> {
420430
override fun run(): Flow<Nothing> = emptyFlow()
421-
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean = otherWorker === FinishedWorker
422431
override fun toString(): String = "FinishedWorker"
423432
}
424433

425-
private class WorkerWrapper<T, R>(
434+
private data class WorkerWrapper<T, R>(
426435
private val wrapped: Worker<T>,
427436
private val flow: Flow<R>
428437
) : Worker<R> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:JvmMultifileClass
17+
@file:JvmName("Workflows")
18+
19+
package com.squareup.workflow
20+
21+
import kotlin.reflect.KClassifier
22+
import kotlin.reflect.KType
23+
import kotlin.reflect.KTypeProjection
24+
import kotlin.reflect.KVariance.OUT
25+
26+
/**
27+
* An implementation of [KType] whose [classifier][KType.classifier] is the `KClass` of the worker
28+
* itself, and which has a type parameter of the [Worker.outputType] of the worker.
29+
*/
30+
internal data class WorkerKType(
31+
override val classifier: KClassifier,
32+
val outputTypeProjection: KTypeProjection
33+
) : KType {
34+
constructor(worker: Worker<*>) : this(worker::class, worker.outputTypeProjection)
35+
36+
override val arguments: List<KTypeProjection> get() = listOf(outputTypeProjection)
37+
override val annotations: List<Annotation> get() = emptyList()
38+
override val isMarkedNullable: Boolean get() = false
39+
40+
override fun toString(): String = "Worker<${outputTypeProjection.type}>"
41+
}
42+
43+
private val Worker<*>.outputTypeProjection: KTypeProjection
44+
get() = outputType?.let { KTypeProjection(OUT, it) } ?: KTypeProjection(null, null)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:JvmMultifileClass
17+
@file:JvmName("Workflows")
18+
19+
package com.squareup.workflow
20+
21+
import kotlinx.coroutines.CoroutineName
22+
import kotlinx.coroutines.flow.Flow
23+
import kotlinx.coroutines.withContext
24+
25+
/**
26+
* TODO write documentation
27+
*/
28+
@OptIn(ExperimentalWorkflowApi::class)
29+
internal class WorkerWorkflow<OutputT>(
30+
workerType: WorkerKType,
31+
private val key: String
32+
) :
33+
StatefulWorkflow<Worker<OutputT>, Int, OutputT, Unit>(),
34+
ImpostorWorkflow {
35+
36+
override val realIdentifier: WorkflowIdentifier = unsnapshottableIdentifier(workerType)
37+
38+
override fun initialState(
39+
props: Worker<OutputT>,
40+
snapshot: Snapshot?
41+
): Int = 0
42+
43+
override fun onPropsChanged(
44+
old: Worker<OutputT>,
45+
new: Worker<OutputT>,
46+
state: Int
47+
): Int = if (!old.doesSameWorkAs(new)) state + 1 else state
48+
49+
override fun render(
50+
props: Worker<OutputT>,
51+
state: Int,
52+
context: RenderContext<Worker<OutputT>, Int, OutputT>
53+
) {
54+
context.runningSideEffect(state.toString()) {
55+
runWorker(props, key, context.actionSink)
56+
}
57+
}
58+
59+
override fun snapshotState(state: Int): Snapshot = Snapshot.EMPTY
60+
}
61+
62+
/**
63+
* TODO write kdoc
64+
*
65+
* Visible for testing.
66+
*/
67+
@OptIn(ExperimentalWorkflowApi::class)
68+
internal suspend fun <OutputT> runWorker(
69+
worker: Worker<OutputT>,
70+
renderKey: String,
71+
actionSink: Sink<WorkflowAction<Worker<OutputT>, Int, OutputT>>
72+
) {
73+
withContext(CoroutineName(worker.debugName(renderKey))) {
74+
worker.runWithNullCheck()
75+
.collectToSink(actionSink) { output ->
76+
action { setOutput(output) }
77+
}
78+
}
79+
}
80+
81+
/**
82+
* In unit tests, if you use a mocking library to create a Worker, the run method will return null
83+
* even though the return type is non-nullable in Kotlin. Kotlin helps out with this by throwing an
84+
* NPE before before any kotlin code gets the null, but the NPE that it throws includes an almost
85+
* completely useless stacktrace and no other details.
86+
*
87+
* This method does an explicit null check and throws an exception with a more helpful message.
88+
*
89+
* See [#842](https://github.com/square/workflow/issues/842).
90+
*/
91+
@Suppress("USELESS_ELVIS")
92+
private fun <T> Worker<T>.runWithNullCheck(): Flow<T> =
93+
run() ?: throw NullPointerException(
94+
"Worker $this returned a null Flow. " +
95+
"If this is a test mock, make sure you mock the run() method!"
96+
)
97+
98+
private fun Worker<*>.debugName(key: String) =
99+
toString().let { if (key.isBlank()) it else "$it:$key" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2019 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.workflow;
17+
18+
import kotlin.reflect.KType;
19+
import kotlinx.coroutines.flow.Flow;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
/**
24+
* Worker that incorrectly returns null from {@link #run}, to simulate the default behavior of some
25+
* mocking libraries.
26+
*
27+
* See <a href="https://github.com/square/workflow/issues/842">#842</a>.
28+
*/
29+
class NullFlowWorker implements Worker {
30+
31+
@Nullable @Override public KType getOutputType() {
32+
return null;
33+
}
34+
35+
@NotNull @Override public Flow run() {
36+
//noinspection ConstantConditions
37+
return null;
38+
}
39+
40+
@Override public boolean doesSameWorkAs(@NotNull Worker otherWorker) {
41+
//noinspection unchecked
42+
return Worker.DefaultImpls.doesSameWorkAs(this, otherWorker);
43+
}
44+
45+
/**
46+
* Override this to make writing assertions on exception messages easier.
47+
*/
48+
@Override public String toString() {
49+
return "NullFlowWorker.toString";
50+
}
51+
}

0 commit comments

Comments
 (0)