/
RenderableAsset.kt
307 lines (267 loc) · 13.8 KB
/
RenderableAsset.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package com.intuit.player.android.asset
import android.content.Context
import android.view.View
import androidx.annotation.StyleRes
import com.intuit.player.android.AndroidPlayer
import com.intuit.player.android.AssetContext
import com.intuit.player.android.DEPRECATED_WITH_DECODABLEASSET
import com.intuit.player.android.build
import com.intuit.player.android.extensions.Style
import com.intuit.player.android.extensions.Styles
import com.intuit.player.android.extensions.removeSelf
import com.intuit.player.android.withContext
import com.intuit.player.android.withStyles
import com.intuit.player.android.withTag
import com.intuit.player.jvm.core.asset.Asset
import com.intuit.player.jvm.core.asset.AssetWrapper
import com.intuit.player.jvm.core.bridge.Node
import com.intuit.player.jvm.core.bridge.NodeWrapper
import com.intuit.player.jvm.core.bridge.serialization.encoding.requireNodeDecoder
import com.intuit.player.jvm.core.player.Player
import com.intuit.player.jvm.core.player.PlayerException
import com.intuit.player.jvm.core.player.state.fail
import com.intuit.player.jvm.core.player.state.inProgressState
import com.intuit.player.plugins.beacon.beacon
import com.intuit.player.plugins.coroutines.subScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.serialization.ContextualSerializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.reflect.KClass
internal typealias CachedAssetView = Pair<AssetContext?, View?>
/**
* [RenderableAsset] is the base class for each asset in an asset tree.
* It is the second stage in the transform process. It's most important
* method is [render], which delegates to [initView] and [hydrate]
* to instantiate a [View] and populate it with the latest [asset] data.
* This approach attempts to optimize by preventing unnecessary [View]
* mutations.
*
* [RenderableAsset]s are powered with an [AssetContext], which provides
* access to the underlying asset node as well as the Android [Context].
* Beaconing and expansion hooks can be accessed through the [AssetContext]
* as well. The asset registry is responsible for creating [RenderableAsset]s
* and can be configured with any factory method to supply [AssetContext]s
* to a new instance. However, it is recommended just propagate the
* [AssetContext] in the constructor as to keep asset registration simple.
*/
@Serializable(RenderableAsset.ContextualSerializer::class)
public abstract class RenderableAsset
@Deprecated(
"RenderableAssets should be migrated to DecodableAsset",
ReplaceWith("DecodableAsset(assetContext, serializer)"),
DeprecationLevel.ERROR,
)
public constructor(public val assetContext: AssetContext) : NodeWrapper {
/**
* Helper to get the current cached [AssetContext] and [View].
* Will return empty pair if not found.
*/
internal val cachedAssetView: CachedAssetView get() =
player.getCachedAssetView(assetContext) ?: cachedAssetViewNotFound
/** Main API */
public val asset: Asset by assetContext::asset
public val player: AndroidPlayer by assetContext::player
public val context: Context? by assetContext::context
override val node: Node by asset
/** Build arbitrary [View] to represent the [asset] */
protected abstract fun initView(): View
/** Hydrate [View] with data from [asset] */
protected abstract fun View.hydrate()
/**
* A [CoroutineScope] that should be used when launching coroutines during asset hydration.
* This scope will be cancelled on each re-render (i.e. whenever the data updates) and when
* the [Player.flowScope] is cancelled.
*/
protected val hydrationScope: CoroutineScope get() = _hydrationScope
?: throw PlayerException("Attempted to use hydrationScope outside hydration context! Ensure usage remains within the RenderableAsset.hydrate function...")
private var _hydrationScope: CoroutineScope?
get() = player.getCachedHydrationScope(assetContext)
set(value) = player.cacheHydrationScope(assetContext, value)
internal fun renewHydrationScope(message: String): CoroutineScope {
_hydrationScope?.cancel(message)
_hydrationScope = player.subScope()
return hydrationScope
}
/**
* Construct a [View] that represents the asset.
*
* The default implementation delegates to [initView] and [hydrate]
* to construct this [View] and populate it with the latest data. It
* also automatically caches the instance of the [View] and detects
* when it needs to reconstruct or rehydrate.
*/
private fun render(): View = cachedAssetView.let { (cachedAssetContext, cachedView) ->
requireContext()
when {
// View not found. Create and hydrate.
cachedView == null -> {
renewHydrationScope("recreating view")
initView().also { it.hydrate() }
} // View found, but contexts are out of sync. Remove cached view and create and hydrate.
cachedAssetContext?.context != context || cachedAssetContext?.asset?.type != asset.type -> {
renewHydrationScope("recreating view")
cachedView.removeSelf()
initView().also { it.hydrate() }
}
// View found, but assets are out of sync. Rehydrate. It is possible for the hydrate
// implementation to throw [StaleViewException] to signify that the view is out of sync.
// This can only be done from invalidateView, so we have a guarantee that the view has
// already been removed from the cache.
!cachedAssetContext.asset.nativeReferenceEquals(asset) ->
try {
cachedView.also(::rehydrate)
} catch (exception: StaleViewException) {
player.logger.info("re-rendering due to stale child: ${exception.assetContext.id}")
render()
}
// View found, everything is in sync. Do nothing.
else -> cachedView
}
}.also { if (it !is SuspendableAsset.AsyncViewStub) player.cacheAssetView(assetContext, it) }
/** Invalidate view, causing a complete re-render of the current asset */
public fun invalidateView() {
player.removeCachedAssetView(assetContext)
throw StaleViewException(assetContext)
}
/** Private helper for managing scope for hydration */
private fun rehydrate(view: View) {
renewHydrationScope("rehydrating ${asset.id}")
view.hydrate()
}
/** Instruct a [RenderableAsset] to [rehydrate] */
public fun rehydrate(): Unit = cachedAssetView.let { (_, view) ->
try {
view?.also(::rehydrate)
} catch (exception: StaleViewException) {
player.inProgressState?.fail("stale child while trying to rehydrate: ${exception.assetContext.id}")
}
}
/**
* Render the asset using the resulting [Context] of the [AndroidPlayer.Hooks.ContextHook]
* called with the provided [context].
*
* This should only be called from the Activity/Fragment to provide a [context] for [RenderableAsset]s to render with.
* Rendering of nested children assets should instead invoke the contextual [RenderableAsset.render] methods
* to automatically pull [context] from their parents.
*/
public fun render(context: Context): View = assetContext
.withContext(player.hooks.context.call(context))
.build()
.render()
/** Render child asset from the context of a parent asset, ensuring that the [context] is passed down */
public fun RenderableAsset.render(): View = assetContext
.withContext(this@RenderableAsset.requireContext())
.build()
.render()
/** Render a [View] with specific [styles] */
public fun RenderableAsset.render(@StyleRes vararg styles: Style?): View = assetContext
.withContext(this@RenderableAsset.requireContext())
.withStyles(*styles)
.build()
.render()
/** Render a [View] with specific [styles] */
public fun RenderableAsset.render(@StyleRes styles: Styles): View = assetContext
.withContext(this@RenderableAsset.requireContext())
.withStyles(styles)
.build()
.render()
/** Render a [View] with a specific [tag] through a new [RenderableAsset] created with a new [AssetContext] */
public fun RenderableAsset.render(tag: String): View = assetContext
.withContext(this@RenderableAsset.requireContext())
.withTag(tag)
.build()
.render()
/** Render a [View] with specific [styles] */
public fun RenderableAsset.render(@StyleRes vararg styles: Style?, tag: String): View = assetContext
.withContext(this@RenderableAsset.requireContext())
.withTag(tag)
.withStyles(*styles)
.build()
.render()
/** Render a [View] with specific [styles] */
public fun RenderableAsset.render(@StyleRes styles: Styles, tag: String): View = assetContext
.withContext(this@RenderableAsset.requireContext())
.withTag(tag)
.withStyles(styles)
.build()
.render()
/** Expansion helpers */
/** Unwraps the [AssetWrapper] extracting [asset] as a [RenderableAsset] */
public fun AssetWrapper.asRenderableAsset(): RenderableAsset? = player.expandAsset(this.asset)
/** Expand [name] as a [RenderableAsset] from the base [asset] */
@Deprecated(DEPRECATED_WITH_DECODABLEASSET, level = DeprecationLevel.ERROR)
@Suppress("DEPRECATION_ERROR")
public fun expand(name: String, context: Context? = this@RenderableAsset.context): RenderableAsset? = asset.expand(name, context)
/** Expand [name] as a [RenderableAsset] from [this] specific [Node] */
@Deprecated(DEPRECATED_WITH_DECODABLEASSET, level = DeprecationLevel.ERROR)
@Suppress("DEPRECATION_ERROR")
public fun Node.expand(name: String, context: Context? = this@RenderableAsset.context): RenderableAsset? = getObject(name)
?.let(::AssetWrapper)
?.run { expand(context) }
/** Expand an [AssetWrapper] with a potentially styled [Context] */
@Deprecated(DEPRECATED_WITH_DECODABLEASSET, level = DeprecationLevel.ERROR)
public fun AssetWrapper.expand(context: Context? = this@RenderableAsset.context): RenderableAsset? = asset
.let { player.expandAsset(it, context) }
/** Expand [name] as a collection of [RenderableAsset]s from the base [asset] */
@Deprecated(DEPRECATED_WITH_DECODABLEASSET, level = DeprecationLevel.ERROR)
@Suppress("DEPRECATION_ERROR")
public fun expandList(name: String, context: Context? = this@RenderableAsset.context): List<RenderableAsset> = asset.expandList(name, context)
/** Expand [name] as a collection of [RenderableAsset]s from [this] specific [Node] */
@Deprecated(DEPRECATED_WITH_DECODABLEASSET, level = DeprecationLevel.ERROR)
@Suppress("DEPRECATION_ERROR")
public fun Node.expandList(name: String, context: Context? = this@RenderableAsset.context): List<RenderableAsset> = getList(name)
?.filterIsInstance<Node>()
?.map(::AssetWrapper)
?.mapNotNull { it.expand(context) } ?: emptyList()
public fun beacon(
action: String,
element: String,
asset: Asset = this.asset,
data: Any? = null,
): Unit = player.beacon(action, element, asset, data)
public fun requireContext(): Context = context ?: run {
val error = PlayerException("Android context not found! Ensure the asset is rendered with a valid Android context.")
player.inProgressState?.fail(error)
throw error
}
/**
* Special interface to be implemented by assets that are meant to fill
* the entire player canvas space regardless of content length
*/
public interface ViewportAsset
private companion object {
private val cachedAssetViewNotFound: Pair<AssetContext?, View?> = null to null
}
public class Serializer(private val player: AndroidPlayer) : KSerializer<RenderableAsset?> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("com.intuit.player.android.asset.RenderableAsset")
/** Deserialize using the expansion process */
override fun deserialize(decoder: Decoder): RenderableAsset? = decoder.requireNodeDecoder()
.decodeNode()
.let(::AssetWrapper)
.asset
.let(player::expandAsset)
/** Serialization of [RenderableAsset]s are not supported */
override fun serialize(encoder: Encoder, value: RenderableAsset?): Nothing =
throw SerializationException("DecodableAsset.Serializer.serialize is not supported")
/** Conform this [Serializer] to cast the expanded asset to [T] */
public inline fun <reified T : RenderableAsset?> conform(): KSerializer<T> = object : KSerializer<T?> by this as KSerializer<T?> {
override fun deserialize(decoder: Decoder) = this@Serializer.deserialize(decoder) as? T
} as KSerializer<T>
public fun <T : RenderableAsset> conform(klass: KClass<T>): KSerializer<T> = object : KSerializer<T?> by this as KSerializer<T?> {
override fun deserialize(decoder: Decoder) = try {
klass.javaObjectType.cast(this@Serializer.deserialize(decoder))
} catch (e: ClassCastException) {
null
}
} as KSerializer<T>
}
// Seemingly needed to prevent stack overflow: https://github.com/Kotlin/kotlinx.serialization/issues/1776
internal object ContextualSerializer : KSerializer<RenderableAsset> by ContextualSerializer(RenderableAsset::class)
}