Skip to content

Commit 54e0b46

Browse files
authored
Merge pull request #1309 from square/ray/unwrap
Punches up composite rendering API and demonstrates nav logging.
2 parents 0c7060a + f76570a commit 54e0b46

File tree

10 files changed

+228
-27
lines changed

10 files changed

+228
-27
lines changed

samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import com.squareup.workflow1.config.AndroidRuntimeConfigTools
1717
import com.squareup.workflow1.ui.Screen
1818
import com.squareup.workflow1.ui.WorkflowLayout
1919
import com.squareup.workflow1.ui.renderWorkflowIn
20+
import com.squareup.workflow1.ui.unwrap
2021
import com.squareup.workflow1.ui.withRegistry
21-
import kotlinx.coroutines.flow.StateFlow
22+
import kotlinx.coroutines.flow.Flow
2223
import kotlinx.coroutines.flow.map
24+
import kotlinx.coroutines.flow.onEach
2325
import timber.log.Timber
2426

2527
private val viewRegistry = SampleContainers
@@ -44,13 +46,15 @@ class PoetryActivity : AppCompatActivity() {
4446
}
4547

4648
class PoetryModel(savedState: SavedStateHandle) : ViewModel() {
47-
val renderings: StateFlow<Screen> by lazy {
49+
val renderings: Flow<Screen> by lazy {
4850
renderWorkflowIn(
4951
workflow = RealPoemsBrowserWorkflow(RealPoemWorkflow()),
5052
scope = viewModelScope,
5153
prop = 0 to 0 to Poem.allPoems,
5254
savedStateHandle = savedState,
5355
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
54-
)
56+
).onEach {
57+
Timber.i("Navigated to %s", it.unwrap())
58+
}
5559
}
5660
}

samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import com.squareup.workflow1.config.AndroidRuntimeConfigTools
1717
import com.squareup.workflow1.ui.Screen
1818
import com.squareup.workflow1.ui.WorkflowLayout
1919
import com.squareup.workflow1.ui.renderWorkflowIn
20+
import com.squareup.workflow1.ui.unwrap
2021
import com.squareup.workflow1.ui.withRegistry
2122
import kotlinx.coroutines.Job
22-
import kotlinx.coroutines.flow.StateFlow
23+
import kotlinx.coroutines.flow.Flow
2324
import kotlinx.coroutines.flow.map
25+
import kotlinx.coroutines.flow.onEach
2426
import kotlinx.coroutines.launch
2527
import timber.log.Timber
2628

@@ -53,7 +55,7 @@ class RavenActivity : AppCompatActivity() {
5355
class RavenModel(savedState: SavedStateHandle) : ViewModel() {
5456
private val running = Job()
5557

56-
val renderings: StateFlow<Screen> by lazy {
58+
val renderings: Flow<Screen> by lazy {
5759
renderWorkflowIn(
5860
workflow = RealPoemWorkflow(),
5961
scope = viewModelScope,
@@ -62,6 +64,8 @@ class RavenModel(savedState: SavedStateHandle) : ViewModel() {
6264
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
6365
) {
6466
running.complete()
67+
}.onEach {
68+
Timber.i("Navigated to %s", it.unwrap())
6569
}
6670
}
6771

samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.squareup.sample.container.overviewdetail
22

33
import com.squareup.workflow1.ui.Screen
4+
import com.squareup.workflow1.ui.Unwrappable
45
import com.squareup.workflow1.ui.navigation.BackStackScreen
56
import com.squareup.workflow1.ui.navigation.plus
67

@@ -19,7 +20,7 @@ class OverviewDetailScreen<out T : Screen> private constructor(
1920
val overviewRendering: BackStackScreen<T>,
2021
val detailRendering: BackStackScreen<T>? = null,
2122
val selectDefault: (() -> Unit)? = null
22-
) : Screen {
23+
) : Screen, Unwrappable {
2324
constructor(
2425
overviewRendering: BackStackScreen<T>,
2526
detailRendering: BackStackScreen<T>
@@ -37,6 +38,12 @@ class OverviewDetailScreen<out T : Screen> private constructor(
3738
operator fun component1(): BackStackScreen<T> = overviewRendering
3839
operator fun component2(): BackStackScreen<T>? = detailRendering
3940

41+
/**
42+
* For nicer logging. See the call to [unwrap][com.squareup.workflow1.ui.unwrap]
43+
* in the activity.
44+
*/
45+
override val unwrapped = detailRendering ?: overviewRendering
46+
4047
override fun equals(other: Any?): Boolean {
4148
if (this === other) return true
4249
if (javaClass != other?.javaClass) return false

samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.squareup.workflow1.ui.AndroidScreen
1414
import com.squareup.workflow1.ui.Screen
1515
import com.squareup.workflow1.ui.ScreenViewFactory
1616
import com.squareup.workflow1.ui.ScreenViewFactory.Companion.map
17+
import com.squareup.workflow1.ui.Unwrappable
1718
import com.squareup.workflow1.ui.navigation.AlertOverlay
1819
import com.squareup.workflow1.ui.navigation.AlertOverlay.Button.NEGATIVE
1920
import com.squareup.workflow1.ui.navigation.AlertOverlay.Button.POSITIVE
@@ -37,12 +38,19 @@ object AreYouSureWorkflow :
3738
): State = snapshot?.toParcelable() ?: Running
3839

3940
class Rendering(
40-
val base: Screen,
41-
val alert: AlertOverlay? = null
42-
) : AndroidScreen<Rendering> {
41+
private val base: Screen,
42+
private val alert: AlertOverlay? = null
43+
) : AndroidScreen<Rendering>, Unwrappable {
4344
override val viewFactory: ScreenViewFactory<Rendering> = map { newRendering ->
4445
BodyAndOverlaysScreen(newRendering.base, listOfNotNull(newRendering.alert))
4546
}
47+
48+
/**
49+
* For nicer logging. See the call to [unwrap][com.squareup.workflow1.ui.unwrap]
50+
* in [HelloBackButtonActivity].
51+
*/
52+
override val unwrapped: Any
53+
get() = alert ?: base
4654
}
4755

4856
@Parcelize

samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ import com.squareup.workflow1.config.AndroidRuntimeConfigTools
1515
import com.squareup.workflow1.ui.Screen
1616
import com.squareup.workflow1.ui.WorkflowLayout
1717
import com.squareup.workflow1.ui.renderWorkflowIn
18+
import com.squareup.workflow1.ui.unwrap
1819
import com.squareup.workflow1.ui.withRegistry
1920
import kotlinx.coroutines.Job
20-
import kotlinx.coroutines.flow.StateFlow
21+
import kotlinx.coroutines.flow.Flow
2122
import kotlinx.coroutines.flow.map
23+
import kotlinx.coroutines.flow.onEach
2224
import kotlinx.coroutines.launch
25+
import timber.log.Timber
2326

2427
private val viewRegistry = SampleContainers
2528

@@ -39,12 +42,18 @@ class HelloBackButtonActivity : AppCompatActivity() {
3942
finish()
4043
}
4144
}
45+
46+
companion object {
47+
init {
48+
Timber.plant(Timber.DebugTree())
49+
}
50+
}
4251
}
4352

4453
class HelloBackButtonModel(savedState: SavedStateHandle) : ViewModel() {
4554
private val running = Job()
4655

47-
val renderings: StateFlow<Screen> by lazy {
56+
val renderings: Flow<Screen> by lazy {
4857
renderWorkflowIn(
4958
workflow = AreYouSureWorkflow,
5059
scope = viewModelScope,
@@ -54,6 +63,8 @@ class HelloBackButtonModel(savedState: SavedStateHandle) : ViewModel() {
5463
// This workflow handles the back button itself, so the activity can't.
5564
// Instead, the workflow emits an output to signal that it's time to shut things down.
5665
running.complete()
66+
}.onEach {
67+
Timber.i("Navigated to %s", it.unwrap())
5768
}
5869
}
5970

workflow-ui/core-android/api/core-android.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ public final class com/squareup/workflow1/ui/navigation/BackButtonScreen : com/s
226226
public synthetic fun getContent ()Ljava/lang/Object;
227227
public final fun getOnBackPressed ()Lkotlin/jvm/functions/Function0;
228228
public final fun getShadow ()Z
229+
public fun getUnwrapped ()Ljava/lang/Object;
229230
public fun getViewFactory ()Lcom/squareup/workflow1/ui/ScreenViewFactory;
230231
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
231232
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper;

workflow-ui/core-common/api/core-common.api

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,27 @@ public final class com/squareup/workflow1/ui/CompatibleKt {
1212
public static final fun compatible (Ljava/lang/Object;Ljava/lang/Object;)Z
1313
}
1414

15-
public abstract interface class com/squareup/workflow1/ui/Container {
15+
public abstract interface class com/squareup/workflow1/ui/Composite : com/squareup/workflow1/ui/Unwrappable {
1616
public abstract fun asSequence ()Lkotlin/sequences/Sequence;
17+
public abstract fun getUnwrapped ()Ljava/lang/Object;
18+
}
19+
20+
public final class com/squareup/workflow1/ui/Composite$DefaultImpls {
21+
public static fun getUnwrapped (Lcom/squareup/workflow1/ui/Composite;)Ljava/lang/Object;
22+
}
23+
24+
public abstract interface class com/squareup/workflow1/ui/Container : com/squareup/workflow1/ui/Composite {
1725
public abstract fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
1826
}
1927

28+
public final class com/squareup/workflow1/ui/Container$DefaultImpls {
29+
public static fun getUnwrapped (Lcom/squareup/workflow1/ui/Container;)Ljava/lang/Object;
30+
}
31+
32+
public final class com/squareup/workflow1/ui/ContainerKt {
33+
public static final fun unwrap (Ljava/lang/Object;)Ljava/lang/Object;
34+
}
35+
2036
public final class com/squareup/workflow1/ui/EnvironmentScreen : com/squareup/workflow1/ui/Screen, com/squareup/workflow1/ui/Wrapper {
2137
public fun <init> (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
2238
public synthetic fun <init> (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -25,6 +41,7 @@ public final class com/squareup/workflow1/ui/EnvironmentScreen : com/squareup/wo
2541
public fun getContent ()Lcom/squareup/workflow1/ui/Screen;
2642
public synthetic fun getContent ()Ljava/lang/Object;
2743
public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment;
44+
public fun getUnwrapped ()Ljava/lang/Object;
2845
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
2946
public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/EnvironmentScreen;
3047
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper;
@@ -49,6 +66,7 @@ public final class com/squareup/workflow1/ui/NamedScreen : com/squareup/workflow
4966
public fun getContent ()Lcom/squareup/workflow1/ui/Screen;
5067
public synthetic fun getContent ()Ljava/lang/Object;
5168
public final fun getName ()Ljava/lang/String;
69+
public fun getUnwrapped ()Ljava/lang/Object;
5270
public fun hashCode ()I
5371
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
5472
public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/NamedScreen;
@@ -70,6 +88,10 @@ public final class com/squareup/workflow1/ui/TextControllerKt {
7088
public static synthetic fun TextController$default (Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/TextController;
7189
}
7290

91+
public abstract interface class com/squareup/workflow1/ui/Unwrappable {
92+
public abstract fun getUnwrapped ()Ljava/lang/Object;
93+
}
94+
7395
public final class com/squareup/workflow1/ui/ViewEnvironment {
7496
public static final field Companion Lcom/squareup/workflow1/ui/ViewEnvironment$Companion;
7597
public fun equals (Ljava/lang/Object;)Z
@@ -138,6 +160,7 @@ public abstract interface class com/squareup/workflow1/ui/Wrapper : com/squareup
138160
public final class com/squareup/workflow1/ui/Wrapper$DefaultImpls {
139161
public static fun asSequence (Lcom/squareup/workflow1/ui/Wrapper;)Lkotlin/sequences/Sequence;
140162
public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/Wrapper;)Ljava/lang/String;
163+
public static fun getUnwrapped (Lcom/squareup/workflow1/ui/Wrapper;)Ljava/lang/Object;
141164
}
142165

143166
public final class com/squareup/workflow1/ui/navigation/AlertOverlay : com/squareup/workflow1/ui/navigation/ModalOverlay {
@@ -218,6 +241,7 @@ public final class com/squareup/workflow1/ui/navigation/BackStackScreen : com/sq
218241
public final fun getFrames ()Ljava/util/List;
219242
public final fun getName ()Ljava/lang/String;
220243
public final fun getTop ()Lcom/squareup/workflow1/ui/Screen;
244+
public fun getUnwrapped ()Ljava/lang/Object;
221245
public fun hashCode ()I
222246
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
223247
public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen;
@@ -241,13 +265,15 @@ public final class com/squareup/workflow1/ui/navigation/BackStackScreenKt {
241265
public static synthetic fun toBackStackScreenOrNull$default (Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/navigation/BackStackScreen;
242266
}
243267

244-
public final class com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen {
268+
public final class com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Composite, com/squareup/workflow1/ui/Screen {
245269
public fun <init> (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;Ljava/lang/String;)V
246270
public synthetic fun <init> (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
271+
public fun asSequence ()Lkotlin/sequences/Sequence;
247272
public final fun getBody ()Lcom/squareup/workflow1/ui/Screen;
248273
public fun getCompatibilityKey ()Ljava/lang/String;
249274
public final fun getName ()Ljava/lang/String;
250275
public final fun getOverlays ()Ljava/util/List;
276+
public fun getUnwrapped ()Ljava/lang/Object;
251277
public final fun mapBody (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen;
252278
public final fun mapOverlays (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen;
253279
}
@@ -258,6 +284,7 @@ public final class com/squareup/workflow1/ui/navigation/FullScreenModal : com/sq
258284
public fun getCompatibilityKey ()Ljava/lang/String;
259285
public fun getContent ()Lcom/squareup/workflow1/ui/Screen;
260286
public synthetic fun getContent ()Ljava/lang/Object;
287+
public fun getUnwrapped ()Ljava/lang/Object;
261288
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Container;
262289
public synthetic fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/Wrapper;
263290
public fun map (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/navigation/FullScreenModal;
@@ -278,5 +305,6 @@ public abstract interface class com/squareup/workflow1/ui/navigation/ScreenOverl
278305
public final class com/squareup/workflow1/ui/navigation/ScreenOverlay$DefaultImpls {
279306
public static fun asSequence (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Lkotlin/sequences/Sequence;
280307
public static fun getCompatibilityKey (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Ljava/lang/String;
308+
public static fun getUnwrapped (Lcom/squareup/workflow1/ui/navigation/ScreenOverlay;)Ljava/lang/Object;
281309
}
282310

workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Container.kt

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,55 @@ package com.squareup.workflow1.ui
33
import com.squareup.workflow1.ui.Compatible.Companion.keyFor
44

55
/**
6-
* A rendering type comprised of a set of other renderings.
6+
* A rendering that wraps another that is actually the interesting
7+
* bit (read: the visible bit), particularly from a logging or testing
8+
* point of view.
79
*
8-
* Why two parameter types? The separate [BaseT] type allows implementations
10+
* This is the easiest way to customize behavior of the [unwrap] function.
11+
*/
12+
public interface Unwrappable {
13+
/** Topmost wrapped content, or `this` if empty. */
14+
public val unwrapped: Any
15+
}
16+
17+
/**
18+
* Handy for logging and testing, extracts the "topmost" bit from a receiving
19+
* workflow rendering, honoring [Unwrappable] if applicable.
20+
*/
21+
public tailrec fun Any.unwrap(): Any {
22+
if (this !is Unwrappable) return this
23+
return unwrapped.unwrap()
24+
}
25+
26+
/**
27+
* A rendering that can be decomposed to a [sequence][asSequence] of others.
28+
*/
29+
public interface Composite<out T> : Unwrappable {
30+
public fun asSequence(): Sequence<T>
31+
32+
public override val unwrapped: Any get() = asSequence().lastOrNull() ?: this
33+
}
34+
35+
/**
36+
* A structured [Composite] rendering comprised of a set of other
37+
* renderings of a [specific type][C] of a particular [category][CategoryT],
38+
* and whose contents can be transformed by [map].
39+
*
40+
* Why two parameter types? The separate [CategoryT] type allows implementations
941
* and sub-interfaces to constrain the types that [map] is allowed to
10-
* transform [C] to. E.g., it allows `FooWrapper<S: Screen>` to declare
42+
* transform [C] to. E.g., it allows `BunchOfScreens<S: Screen>` to declare
1143
* that [map] is only able to transform `S` to other types of `Screen`.
1244
*
13-
* @param BaseT the invariant base type of the contents of such a container,
45+
* @param CategoryT the invariant base type of the contents of such a container,
1446
* usually [Screen] or [Overlay][com.squareup.workflow1.ui.navigation.Overlay].
15-
* It is common for the [Container] itself to implement [BaseT], but that is
47+
* It is common for the [Container] itself to implement [CategoryT], but that is
1648
* not a requirement. E.g., [ScreenOverlay][com.squareup.workflow1.ui.navigation.ScreenOverlay]
1749
* is an [Overlay][com.squareup.workflow1.ui.navigation.Overlay], but it
1850
* wraps a [Screen].
1951
*
20-
* @param C the specific subtype of [BaseT] collected by this [Container].
52+
* @param C the specific subtype of [CategoryT] collected by this [Container].
2153
*/
22-
public interface Container<BaseT, out C : BaseT> {
23-
public fun asSequence(): Sequence<C>
24-
54+
public interface Container<CategoryT, out C : CategoryT> : Composite<C> {
2555
/**
2656
* Returns a [Container] with the [transform]ed contents of the receiver.
2757
* It is expected that an implementation will take advantage of covariance
@@ -41,7 +71,7 @@ public interface Container<BaseT, out C : BaseT> {
4171
* val childBackStackScreen = renderChild(childWorkflow) { ... }
4272
* val loggingBackStackScreen = childBackStackScreen.map { LoggingScreen(it) }
4373
*/
44-
public fun <D : BaseT> map(transform: (C) -> D): Container<BaseT, D>
74+
public fun <D : CategoryT> map(transform: (C) -> D): Container<CategoryT, D>
4575
}
4676

4777
/**
@@ -50,9 +80,9 @@ public interface Container<BaseT, out C : BaseT> {
5080
* [EnvironmentScreen][com.squareup.workflow1.ui.EnvironmentScreen] that allows
5181
* changes to be made to the [ViewEnvironment].
5282
*
53-
* Usually a [Wrapper] is [Compatible] only with others of the same type with
54-
* [Compatible] [content]. In aid of that, this interface extends [Compatible] and
55-
* provides a convenient default implementation of [compatibilityKey].
83+
* Usually a [Wrapper] is [Compatible] only with others that are of the same type
84+
* and which are holding [Compatible] [content]. In aid of that, this interface extends
85+
* [Compatible] and provides a convenient default implementation of [compatibilityKey].
5686
*/
5787
public interface Wrapper<BaseT : Any, out C : BaseT> : Container<BaseT, C>, Compatible {
5888
public val content: C

workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/BodyAndOverlaysScreen.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.squareup.workflow1.ui.navigation
22

33
import com.squareup.workflow1.ui.Compatible
44
import com.squareup.workflow1.ui.Compatible.Companion.keyFor
5+
import com.squareup.workflow1.ui.Composite
56
import com.squareup.workflow1.ui.Screen
67

78
/**
@@ -75,9 +76,11 @@ public class BodyAndOverlaysScreen<B : Screen, O : Overlay>(
7576
public val body: B,
7677
public val overlays: List<O> = emptyList(),
7778
public val name: String = ""
78-
) : Screen, Compatible {
79+
) : Screen, Compatible, Composite<Any> {
7980
override val compatibilityKey: String = keyFor(this, name)
8081

82+
override fun asSequence(): Sequence<Any> = sequenceOf(body) + overlays.asSequence()
83+
8184
public fun <S : Screen> mapBody(transform: (B) -> S): BodyAndOverlaysScreen<S, O> {
8285
return BodyAndOverlaysScreen(transform(body), overlays, name)
8386
}

0 commit comments

Comments
 (0)