-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Plugins android clean up #290
Changes from 12 commits
7df8037
dadc9bc
9fdfb11
9bc4913
a69d22a
6ff5ba5
aa19668
97a2537
eb78cb1
d3a32f0
2b6b7e4
7326685
064ce95
959d444
dfc5b80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
--- | ||
title: AsyncNode Plugin | ||
platform: core,react,ios,android | ||
--- | ||
|
||
# Async Node Plugin | ||
|
||
The AsyncNode Plugin is used to enable streaming additional content into a flow that has already been loaded and rendered. | ||
A common use case for this plugin is conversational UI, as the users input more dialogue, new content must be streamed into Player in order to keep the UI up to date. | ||
|
||
The pillar that makes this possible is the concept of an `AsyncNode`. An `AsyncNode` is any node with the property `async: true`, it represents a placeholder node that will be replaced by a concrete node in the future. | ||
|
||
In the example below, node with the id "some-async-node" will not be rendered on first render, but will be replaced with a concrete asset node at a later time: | ||
|
||
```json | ||
{ | ||
"id": "flow-1", | ||
"views": [ | ||
{ | ||
"id": 'action', | ||
"actions": [ | ||
{ | ||
"id": "some-async-node", | ||
"async": true, | ||
}, | ||
], | ||
}, | ||
], | ||
... | ||
} | ||
``` | ||
|
||
The `AsyncNodePlugin` exposes an `onAsyncNode` hook on all platforms. The `onAsyncNode` hook will be invoked with the current node when the plugin is available and an `AsyncNode` is detected during the resolve process. The node used to call the hook with could contain metadata according to content spec. | ||
|
||
User should tap into the `onAsyncNode` hook to examine the node's metadata before making a decision on what to replace the async node with. The return could be a single node or an array of nodes. | ||
|
||
|
||
### Continuous Streaming | ||
|
||
In order to keep streaming in new content, there must be at least 1 or more `AsyncNode`s in the view tree at all times. | ||
This means there must be a constant renewal of new `AsyncNode`s after the previous ones are resolved by the user. | ||
|
||
## Usage | ||
|
||
<PlatformTabs> | ||
<core> | ||
|
||
Add the plugin to Player: | ||
|
||
```js | ||
import { Player } from '@player-ui/player'; | ||
import { AsyncNodePlugin } from '@player-ui/async-node-plugin'; | ||
|
||
const asyncNodePlugin = new AsyncNodePlugin(); | ||
|
||
// Configuring async node behaviour | ||
asyncNodePlugin.hooks.onAsyncNode.tap('handleAsync', async (node: Node.Node) => { | ||
... | ||
// Determine what to return to be parsed into a concrete node | ||
}); | ||
|
||
const player = new Player({ | ||
plugins: [ | ||
asyncNodePlugin | ||
] | ||
}) | ||
``` | ||
</core> | ||
<react> | ||
|
||
The `react` version of the AsyncNodePlugin is identical to using the core plugin. Refer to core usage for handler configuration: | ||
|
||
```js | ||
import { ReactPlayer } from '@player-ui/react'; | ||
import { MetricsPlugin } from '@player-ui/async-node-plugin'; | ||
|
||
const asyncNodePlugin = new AsyncNodePlugin(); | ||
|
||
const player = new ReactPlayer({ | ||
plugins: [ | ||
asyncNodePlugin | ||
] | ||
}) | ||
``` | ||
|
||
</react> | ||
<ios> | ||
|
||
</ios> | ||
<android> | ||
|
||
In build.gradle | ||
```kotlin | ||
implementation "com.intuit.player.plugins:async-node:$JVM_PLAYER_VERSION" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just |
||
``` | ||
|
||
In integration code | ||
```kotlin | ||
import com.intuit.player.plugins.asyncnode.AsyncNodePlugin | ||
|
||
val asyncNodePlugin = AsyncNodePlugin() | ||
|
||
// Configuring async node behaviour | ||
asyncNodePlugin.hooks.onAsyncNode.tap("handleAsync") { hookContext, node -> | ||
... | ||
// Determine what to return in the form of a list of maps representing nodes that can be parsed | ||
// e.g. | ||
// listOf( | ||
// mapOf( | ||
// "asset" to mapOf( | ||
// "id" to "asset-1", | ||
// "type" to "text", | ||
// "value" to "new asset!" | ||
// ) | ||
// ) | ||
// ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't put this in terms of nodes -- that means little to consumers. Really, what they're interested in is UI, so asset representation. This goes for the above docs too. I'd just put it in terms of content, which is really what you have here, but it's worth calling out. Maybe there can be a sentence or two calling out that this'd work (?) for replacing non-asset nodes (like metadata or something), if that's applicable. Not sure there is a great use case for that though. |
||
} | ||
|
||
AndroidPlayer(asyncNodePlugin) | ||
``` | ||
</android> | ||
</PlatformTabs> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -186,24 +186,37 @@ For convenience, there are several extension methods for utilizing a pre-install | |
|
||
```kotlin | ||
// registering handler | ||
player.onBeacon { beacon: String -> | ||
androidPlayer.onBeacon { beacon: String -> | ||
// Process beacon and send to analytics platform | ||
} | ||
|
||
// fire beacon | ||
player.beacon( | ||
androidPlayer.beacon( | ||
"clicked", | ||
"button", | ||
assetThatIsBeaconing, | ||
someData, | ||
) | ||
``` | ||
|
||
The base `RenderableAsset` class provides an additional helper to make beaconing less verbose from an asset perspective. | ||
The base `RenderableAsset` class provides an additional helper as well as extension to make beaconing less verbose from an asset perspective. | ||
|
||
```kotlin | ||
// within RenderableAsset implementation | ||
beacon(BeaconAction.clicked, BeaconElement.button) | ||
// helper in RenderableAsset | ||
public fun beacon( | ||
action: String, | ||
element: String, | ||
asset: Asset = this.asset, | ||
data: Any? = null | ||
): Unit | ||
|
||
// extension on RenderableAsset | ||
fun RenderableAsset.beacon( | ||
action: BeaconAction, | ||
element: BeaconElement, | ||
asset: Asset = this.asset, | ||
data: Any? = null | ||
): Unit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we just link to the source here to ensure it stays up-to-date? |
||
``` | ||
|
||
</android> | ||
|
@@ -288,4 +301,40 @@ struct ActionAssetView: View { | |
``` | ||
|
||
</ios> | ||
|
||
<android> | ||
|
||
Using the base `RenderableAsset`'s helper function in this example fires off a basic beacon with no adjustment to format as well as empty data upon button click | ||
|
||
**Example** | ||
|
||
```kotlin | ||
class MyAsset(assetContext: AssetContext) : | ||
DecodableAsset<>(assetContext, Data.serializer()) { | ||
|
||
@Serializable | ||
data class Data( | ||
... | ||
) : AssetData() | ||
|
||
override fun createView(): View { | ||
... | ||
} | ||
|
||
override fun View.hydrate() { | ||
val binding = MyCustomViewBinding.bind(this) | ||
binding.button.setOnClickListener { | ||
[email protected]( | ||
action = BeaconAction.clicked, | ||
element = BeaconElement.button, | ||
asset = this@MyAsset, | ||
data = null | ||
) | ||
} | ||
... | ||
} | ||
} | ||
``` | ||
|
||
</android> | ||
</PlatformTabs> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
--- | ||
title: Coroutines | ||
platform: android | ||
--- | ||
|
||
# Kotlin Coroutines | ||
|
||
Kotlin coroutines is a popular Kotlin library to handle asynchronous programming and concurrency in the JVM. More information on Coroutines can be found [here](https://kotlinlang.org/docs/coroutines-overview.html) | ||
This module contains several JVM exclusive plugins to aid various tooling needs. | ||
|
||
## UpdatesPlugin | ||
|
||
Plugin using a `ReceiveChannel` to provide an updated asset whenever the Player View is updated. | ||
|
||
### Usage | ||
|
||
<PlatformTabs> | ||
sugarmanz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<android> | ||
In build.gradle | ||
|
||
```kotlin | ||
implementation "com.intuit.player.plugins:coroutines:$JVM_PLAYER_VERSION" | ||
``` | ||
|
||
In Player constructor | ||
```kotlin | ||
import com.intuit.player.plugins.coroutines.UpdatesPlugin | ||
|
||
val plugins = listOf(UpdatesPlugin()) | ||
AndroidPlayer(plugins) | ||
``` | ||
</android> | ||
</PlatformTabs> | ||
|
||
|
||
### API | ||
|
||
<PlatformTabs> | ||
<android> | ||
The API for this plugin revolves around the `ReceiveChannel` for asset updates. | ||
|
||
```kotlin | ||
// Plugin API | ||
|
||
/** Public property that is used by the [UpdatesPlugin] to send [Asset] updates, can be used in a CoroutineContext to receive [Asset] updates. */ | ||
public val updates: ReceiveChannel<Asset?> | ||
|
||
/** This blocks the current thread until a new update is received or the timeout specified has passed. */ | ||
public fun waitForUpdates(timeout: Long = 500): Asset? | ||
|
||
// Experimental | ||
|
||
/** This cleans out the [updates] channel to prepare it to receive new updates */ | ||
public fun flush(): Unit | ||
|
||
// Player extensions | ||
public val Player.updatesPlugin: UpdatesPlugin? | ||
public fun Player.waitForUpdates(timeout: Long = 500): Asset? | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment around just linking to source - if we want separate API docs, we should just generate them |
||
|
||
</android> | ||
</PlatformTabs> | ||
|
||
## FlowScopePlugin | ||
|
||
Plugin that provides a CoroutineScope that can be used to perform various async operations with the context of the current flow. | ||
All operations launched using the scope will be cancelled when player changes state. | ||
|
||
### Usage | ||
|
||
<PlatformTabs> | ||
<android> | ||
In build.gradle | ||
|
||
```kotlin | ||
implementation "com.intuit.player.plugins:coroutines:$JVM_PLAYER_VERSION" | ||
``` | ||
|
||
In Player constructor | ||
```kotlin | ||
import com.intuit.player.plugins.coroutines.FlowScopePlugin | ||
|
||
val plugins = listOf(FlowScopePlugin()) | ||
AndroidPlayer(plugins) | ||
``` | ||
</android> | ||
</PlatformTabs> | ||
|
||
|
||
### API | ||
|
||
<PlatformTabs> | ||
<android> | ||
|
||
```kotlin | ||
// Plugin API | ||
|
||
/** The CoroutineScope reflective of the current Player flow */ | ||
public var flowScope: CoroutineScope? | ||
/** Build a child scope of the current flowScope to ensure that these scopes will be structured according to the current Flow */ | ||
public fun subScope(coroutineContext: CoroutineContext = Dispatchers.Default): CoroutineScope? | ||
|
||
// Extension functions | ||
|
||
/** Retrieves the current Player flow bound to the scope created by this plugin */ | ||
public val CoroutineScope.flow: Flow? | ||
public val Player.flowScopePlugin: FlowScopePlugin? | ||
public val Player.flowScope: CoroutineScope? | ||
public fun Player.subScope(coroutineContext: CoroutineContext = EmptyCoroutineContext): CoroutineScope? | ||
``` | ||
</android> | ||
</PlatformTabs> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both
core
andreact
are the same, can we consolidate the tabs? Like just havereact
or just havecore
? This kinda goes back to the difference between headless and UI/platform plugins. Like theAsyncNodePlugin
doesn't have areact
specific version in source either.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wondered about this as well, idk what the tab would be.
core
withoutreact
may not be clear to web consumers that they can use it, butreact
withoutcore
is incorrect cuz it's a core plugin