Skip to content

Commit d8b94ce

Browse files
authored
Supplement Android + JVM platform documentation (#719)
* android + jvm platform docs * update [email protected] * address comments
1 parent 373994c commit d8b94ce

File tree

12 files changed

+233
-121
lines changed

12 files changed

+233
-121
lines changed

MODULE.bazel

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ module(
66
bazel_dep(name = "rules_player")
77
archive_override(
88
module_name = "rules_player",
9-
integrity = "sha256-7m/eHQy+Gc0y+EBnEugRGrPw0ss9wz3xTCsr5qHBFIU=",
10-
strip_prefix = "rules_player-2.0.1",
11-
urls = ["https://github.com/player-ui/rules_player/archive/refs/tags/v2.0.1.tar.gz"],
9+
integrity = "sha256-90jZ8zCyOrUIwYMPH5kb4renSuDeZQ8vXciKhYC7khM=",
10+
strip_prefix = "rules_player-2.1.1",
11+
urls = ["https://github.com/player-ui/rules_player/archive/refs/tags/v2.1.1.tar.gz"],
1212
)
1313
bazel_dep(name = "platforms", version = "1.0.0")
1414
bazel_dep(name = "aspect_bazel_lib", version = "2.19.4")

MODULE.bazel.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/site/astro.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default defineConfig({
4747
"/plugins/common-types": resolvedPath("/plugins/core/common-types/"),
4848
"/tools/cli": resolvedPath("/capabilities/cli/"),
4949
"/tools/storybook": resolvedPath("/capabilities/storybook/"),
50+
"/assets/cross-platform/": resolvedPath("/platforms/cross-platform"),
5051
},
5152
integrations: [
5253
react(),
@@ -99,6 +100,10 @@ export default defineConfig({
99100
label: "Guides",
100101
autogenerate: { directory: "guides" },
101102
},
103+
{
104+
label: "Platforms",
105+
autogenerate: { directory: "platforms" },
106+
},
102107
{
103108
label: "Authoring",
104109
autogenerate: { directory: "authoring" },
144 KB
Loading

docs/site/src/content/docs/assets/reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Alongside distributing the Android Player framework, there is a reference assets
5454
```kotlin
5555
import com.intuit.playerui.android.reference.assets.ReferenceAssetsPlugin
5656

57-
val player = AndroidPlayer(context, ReferenceAssetsPlugin())
57+
val player = AndroidPlayer(ReferenceAssetsPlugin())
5858
```
5959

6060
</Fragment>

docs/site/src/content/docs/getting-started.mdx

Lines changed: 4 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,6 @@ dependencies {
4646
implementation("com.intuit.playerui.plugins:reference-assets:$playerVersion")
4747
}
4848
```
49-
50-
#### Release Optimization
51-
52-
The Android Player dependency automatically pulls in both the `dev` as well as the `prod` versions of the underlying JS scripts.
53-
54-
The `dev` scripts are handy for debugging shared JS code through the JS debugger in debug builds, but they are not necessary for consumer facing release builds.
55-
56-
To optimize the release bundles, it is highly recommended to exclude the `dev` scripts, as well as enabling V8 to strip out unused code from the final bundle.
57-
58-
```kotlin
59-
buildTypes {
60-
getByName("release") {
61-
isMinifyEnabled = true
62-
isShrinkResources = true
63-
packagingOptions {
64-
exclude("**/*.dev.js")
65-
}
66-
}
67-
}
68-
```
69-
7049
</Fragment>
7150
</PlatformTabs>
7251

@@ -114,16 +93,14 @@ var body: some View {
11493
```kotlin
11594
// create Android Player with reference assets plugin
11695
val player = AndroidPlayer(
117-
listOf(
118-
ReferenceAssetsPlugin(),
119-
// Any other plugins
120-
)
96+
ReferenceAssetsPlugin(),
97+
// Any other plugins
12198
)
12299
```
123100

124101
Apart from providing a list of plugins while setting up `AndroidPlayer`, you can also provide a `config` object that has the following options:
125102

126-
- `debuggable` - Indicates if the runtime is debuggable on android through chromium devtools, enabling this would let you set breakpoints in js code as you're running it on android.
103+
- `debuggable` - Indicates Player should instrument its `debuggable` constructs
127104
- `coroutineExceptionHandler` - [CoroutineExceptionHandler](https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler) should handle all uncaught exceptions while using the runtime coroutine scope.
128105
- `timeout` - Timeout for the JS thread. If none is provided then it is set as `if (debuggable) Int.MAX_VALUE.toLong() else 5000`.
129106

@@ -191,78 +168,7 @@ player.onUpdate { asset: RenderableAsset? ->
191168
player.start(content)
192169
```
193170

194-
When you're done with Player, release any native runtime memory used to instantiate Player or run the flow.
195-
196-
```kotlin
197-
player.release()
198-
```
199-
200-
Creating your own `AndroidPlayer` instance is fairly simple, but it grows in complexity when considering proper resources management and app orchestration. We provide a Fragment/ViewModel integration to make it easier to properly integrate into your app and account for these concerns.
201-
202-
#### ViewModel
203-
204-
With the `PlayerViewModel` and `PlayerFragment`, the above orchestration is done for you. All that is needed is to provide concrete implementations of each and add the fragment to your app.
205-
206-
##### PlayerViewModel
207-
208-
The `PlayerViewModel` requires an `AsyncFlowIterator` to be supplied in the constructor. The AsyncFlowIterator is what tells Player which flows to run. This can be hardcoded into the view model or expected as an argument, as shown below.
209-
210-
```kotlin
211-
class SimplePlayerViewModel(flows: AsyncFlowIterator) : PlayerViewModel(flows) {
212-
override val plugins = listOf(ReferenceAssetsPlugin())
213-
}
214-
```
215-
216-
##### PlayerFragment
217-
218-
The `PlayerFragment` is a simple Android `Fragment` that only requires a specific `PlayerViewModel` to be defined. If your view model requires the `AsyncFlowIterator` to be passed as part of the constructor, you can leverage the `PlayerViewModel.Factory` to produce it, as shown below.
219-
220-
Specifically, this fragment takes a flow as an argument to the constructor and creates a single-flow `AsyncFlowIterator` instance using the pseudo-constructor helper.
221-
222-
```kotlin
223-
class SimplePlayerFragment(override val flow: String) : PlayerFragment() {
224-
override val playerViewModel by viewModels<SimplePlayerViewModel> {
225-
PlayerViewModel.Factory(AsyncFlowIterator(flow), ::SimplePlayerViewModel)
226-
}
227-
}
228-
```
229-
230-
#### JS Runtime
231-
232-
As the core Player is written in TypeScript, we need a JVM compatible JavaScript runtime to power a JVM based Player. There are several options to choose from, however, deciding on a specific runtime implementation is difficult to do at the library layer. Different use cases may demand different trade-offs regarding supporting multiple platforms, size, and speed. Thus, the base JVM Player implementation was done with a custom runtime abstraction, powered by [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization), to enable consumers to choose the runtime based on their needs.
233-
234-
To support a specific runtime implementation, there needs to be code connecting the runtime constructs to the Player runtime abstraction. The Player project has implemented this layer for several runtimes, described below. It is possible to define your own, but the abstraction definition is not yet final, and therefore not appropriately documented. You can take inspiration from the existing implementations, or file an issue for a runtime you wish to see supported by our team.
235-
236-
| Runtimes | Platforms |
237-
| --------------------------------------------- | --------------------------- |
238-
| [J2V8](https://github.com/eclipsesource/J2V8) | `android`, `linux`, `macos` |
239-
| [Hermes](https://github.com/facebook/hermes) | `android` |
240-
| [GraalJS](https://github.com/oracle/graaljs) | `linux`, `macos`, `win` |
241-
242-
:::note
243-
Each of the artifacts for the above are defined as `com.intuit.playerui:$runtime-$platform`, i.e. `com.intuit.playerui:j2v8-android`
244-
:::
245-
246-
The `HeadlessPlayer` does not define a hard dependency on any specific runtime, however, the `AndroidPlayer` does transitively depends on the `j2v8` runtime, as the first class approach. To override, the transitive dependency would need to be explicitly excluded and the actual runtime dependency would need to be added:
247-
248-
```kotlin
249-
dependencies {
250-
// Android Player dependency
251-
implementation("com.intuit.playerui", "android", PLAYER_VERSION) {
252-
// J2V8 included for release versions
253-
exclude(group = "com.intuit.playerui", module = "j2v8-android")
254-
// Debuggable J2V8 included for canary versions
255-
exclude(group = "com.intuit.playerui", module = "j2v8-android-debug")
256-
}
257-
// Override with Hermes runtime for example
258-
implementation("com.intuit.playerui", "hermes-android", PLAYER_VERSION)
259-
}
260-
```
261-
262-
:::caution
263-
If your application includes dependencies that may transitively depend on `com.intuit.playerui:android`, you would likely need to ensure the default runtime is transitively excluded from those as well, either manually or as a global strategy.
264-
:::
265-
171+
Creating your own `AndroidPlayer` instance is fairly simple, but it grows in complexity when considering proper resources management and app orchestration. We provide a [Fragment/ViewModel integration](/platforms/android#viewmodel) to make it easier to properly integrate into your app and account for these concerns.
266172
</Fragment>
267173
</PlatformTabs>
268174

docs/site/src/content/docs/guides/multi-flow-experiences.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,7 @@ struct App: View {
100100
```
101101

102102
</Fragment>
103+
<Fragment slot='android'>
104+
On Android, the Managed Player paradigm is currently realized through the `PlayerFragment` and `PlayerViewModel` constructs. More information can be found in the [Android documentation](/platforms/android#viewmodel).
105+
</Fragment>
103106
</PlatformTabs>

docs/site/src/content/docs/guides/writing-plugins.mdx

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ platform: core,react
44
---
55

66
import PlatformTabs from "../../../components/PlatformTabs.astro";
7+
import Image from "../../../components/Image.astro";
8+
import pluginsOOO from "../../../assets/plugin-ooo.png";
79

810
While we have published a majority of the plugins we have developed, there will always be new use cases that may require new functionality. Writing a plugin in the easiest way to extend Player functionality for these cases. Plugins work slightly differently on each platform so in this guide we will cover how to write a plugin for each platform.
911

@@ -56,6 +58,9 @@ For a more comprehensive guide on plugins, check out this [Plugin Implementation
5658

5759
_Note: For the React Player you can import and load the plugin the same way you would a React Player Plugin but for the iOS and Android Players you will need to wrap the javascript bundle in a iOS/Android plugin to ensure it is available on your platform._
5860

61+
:::caution
62+
Even though core plugins are written in TypeScript, they're not targeting web environments, which limits the common [global web APIs](https://developer.mozilla.org/en-US/docs/Web/API) that TS developers are typically used to. Avoid web-specific APIs in core plugins unless absolutely necessary -- polyfills, or native implementations may be required for the core plugin to function on mobile platforms.
63+
:::
5964
</Fragment>
6065
<Fragment slot='react'>
6166

@@ -105,6 +110,8 @@ Lastly React plugins can also act as a core plugin in cases where core functiona
105110

106111
</Fragment>
107112
<Fragment slot='ios'>
113+
## iOS Plugins
114+
108115
iOS Player Plugins are very similar to core and react plugins in both their composition and use.
109116

110117
### NativePlugin
@@ -135,23 +142,7 @@ class EnvironmentPlugin: NativePlugin {
135142
}
136143
```
137144

138-
#### Asset Registration
139-
140-
Likely the most common usecase for plugins is to register assets:
141-
142-
```swift
143-
import PlayerUI
144-
145-
class ExampleAssetPlugin: NativePlugin {
146-
let pluginName = "ExampleAssetPlugin"
147-
148-
func apply<P>(player: P) where P: HeadlessPlayer {
149-
guard let player = player as? SwiftUIPlayer else { return }
150-
player.assetRegistry.register("text", asset: TextAsset.self)
151-
player.assetRegistry.register("action", asset: ActionAsset.self)
152-
}
153-
}
154-
```
145+
Likely the most common usecase for plugins is to provide UI through assets. More information can be found in the [Custom Assets guide](/assets/custom).
155146

156147
### JSBasePlugin
157148

@@ -211,5 +202,69 @@ class SharedJSPlugin: JSBasePlugin {
211202

212203
**Note**: `JSBasePlugin` implementations do not necessarily need to be a `PlayerPlugin`, for example, the [BeaconPlugin](https://github.com/player-ui/player/blob/main/plugins/beacon/ios/Sources/BaseBeaconPlugin.swift#L59) can take plugins in it's constructor, that are not `PlayerPlugin`.
213204

205+
</Fragment>
206+
<Fragment slot="android">
207+
## Android Plugins
208+
209+
For all types of plugins, the `apply(instance: T)` signature is the standard way of getting the instance of whatever the plugin is applying to. It is best practice that this method remains idempotent, but that doesn't mean that plugins can't maintain state to hold a reference to said `instance`. Different types of plugins are handled at different stages of initialization, but the order of the same types of plugins will be preserved when applying the plugins.
210+
211+
### Android Player Plugins
212+
213+
Android Player plugins must implement the `AndroidPlayerPlugin` interface, which will give them access to an `AndroidPlayer` instance through the `apply` method. Plugins can use this instance to register UI assets, apply themes, or even access the [Player hooks](#jvm-player-plugins).
214+
215+
Likely the most common usecase for plugins is to provide UI through assets. More information can be found in the [Custom Assets guide](/assets/custom).
216+
217+
### JVM Player Plugins
218+
219+
Player plugins that are only required for the JVM use cases should implement the `PlayerPlugin` interface. This provides a _limited_ `Player` wrapper of the core JS Player, which are exposed through the Player `hooks`. A basic example of `PlayerPlugin` to handle errors:
220+
221+
```kotlin
222+
class HandleErrorPlayerPlugin : PlayerPlugin {
223+
override fun apply(player: Player) {
224+
player.hooks.state.tap { state ->
225+
if (state is ErrorState) {
226+
// handle error
227+
}
228+
}
229+
}
230+
}
231+
```
232+
233+
For more practical examples, take a look at the [coroutines plugins](/plugins/android/coroutines/) which tie asynchronous Player paradigms to [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) constructs.
234+
235+
:::note
236+
The `AndroidPlayer` also extends the "core" JVM `Player`, which means that Android Player plugins already have access to everything the `PlayerPlugin` provides.
237+
:::
238+
239+
### JS Player Plugins
240+
241+
Given the core Player is written in TypeScript incidentally means that most Player plugins will also be written in TypeScript. Loading these on the JVM platform requires some additional setup. At the very least, the plugin will need to be bundled with its dependencies and transpiled according the the target [JS runtime](/platforms/jvm#javascript-runtime).
242+
243+
Once you have your bundle, all that is left is implementing the wrapper on the JVM. The plugin wrapper should implement the `JSPluginWrapper`, which includes an `apply(runtime: Runtime)` method to provide the `Runtime` in which to instantiate the JS Player plugin within.
244+
245+
For plugins that do not have any constructor arguments, the `JSScriptPluginWrapper` can be used to simplify the overhead of instantiated that plugin. It is an abstract class that implements of the `JSPluginWrapper`. This can be instantiated with the loaded bundle as a string or the location of the bundle on the classpath and will automatically read the bundle and instantiate the plugin within the JS runtime.
246+
247+
```kotlin
248+
class MyCorePlugin : JSScriptPluginWrapper("MyCorePluginName", sourcePath = "path/to/source.js") {
249+
// Expose a member of the core instance
250+
public val someField: String by NodeSerializableField(String.serializer())
251+
252+
// Privately declare loosely typed-function of the core instance
253+
private val someFunc: Invokable<String> by NodeSerializableFunction()
254+
// Expose strictly typed method for consumption
255+
public fun someFunc(p1: String): String = someFunc(p1)
256+
}
257+
```
258+
259+
### Runtime Plugins
260+
261+
`RuntimePlugin`s are supertypes of the `JSPluginWrapper` for JS Player plugin wrappers. At this layer, they can be used to add non-player specific functionality to the JS runtime. For example, the [`BeaconPlugin`](/plugins/multiplatform/beacon/) requires the `setTimeout` global method, which does not exist by default in some runtimes. Thus, the `BeaconPlugin` applies the `SetTimeoutPlugin`, which is a `RuntimePlugin`, before it instantiates the actual JS Player beacon plugin.
262+
263+
### Plugin Order of Operations
264+
265+
With all these different types of plugins, it may be difficult to understand how they are treated and when each is applied. Each of these plugin types implements the base `Plugin` interface, which provides a bounding layer for any type of plugin. Both the Android and JVM Players accept a collection of `Plugin`s, which may or may not be specific to that Player. This allows non-player plugins, such as the `RuntimePlugin` to be passed into the instantiation of either Player, simplifying the overhead of using such plugins. During the different layers of initialization, the internals will handle each type of plugin that it cares about. Because the internals are expecting certain types of plugins, any direct implementation of the base `Plugin` is certainly an error. Below is a detailed sequence diagram describing the order in which plugins are applied.
266+
267+
<Image darkModeInvert src={pluginsOOO} alt="Plugin Order of Operations" />
268+
214269
</Fragment>
215270
</PlatformTabs>

0 commit comments

Comments
 (0)