Skip to content

Commit

Permalink
Merge pull request #73 from KevinnZou/feature/jsbridge_support Suppor…
Browse files Browse the repository at this point in the history
…t communication between Native and JS

Support communication between Native and JS
  • Loading branch information
KevinnZou authored Dec 27, 2023
2 parents 329eff7 + 1b30695 commit c0bd75b
Show file tree
Hide file tree
Showing 42 changed files with 902 additions and 100 deletions.
89 changes: 78 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,27 +245,94 @@ Column {
val loadingState = state.loadingState
if (loadingState is LoadingState.Loading) {
LinearProgressIndicator(
progress = loadingState.progress,
modifier = Modifier.fillMaxWidth()
progress = loadingState.progress,
modifier = Modifier.fillMaxWidth()
)
}
WebView(
state = state,
navigator = navigator
)
WebView(
state = state,
navigator = navigator
)
}
```

## Communication between WebView and Native

Starting from version 1.8.0, this library provides a `WebViewJsBridge` to allow developers to
communicate between the WebView and Native.
Developers can use the JsBridge to register a handler to handle the message from the WebView.

```kotlin
val jsBridge = rememberWebViewJsBridge()

LaunchedEffect(jsBridge) {
jsBridge.register(GreetJsMessageHandler())
}
```

The handler should implement the `IJsMessageHandler` interface.

```kotlin
interface IJsMessageHandler {
fun methodName(): String

fun canHandle(methodName: String) = methodName() == methodName

fun handle(
message: JsMessage,
callback: (String) -> Unit,
)

}

class GreetJsMessageHandler : IJsMessageHandler {
override fun methodName(): String {
return "Greet"
}

override fun handle(message: JsMessage, callback: (String) -> Unit) {
Logger.i {
"Greet Handler Get Message: $message"
}
val param = processParams<GreetModel>(message)
val data = GreetModel("KMM Received ${param.message}")
callback(dataToJsonString(data))
}
}
```

Developers can use the `window.kmpJsBridge.callNative` to send a message to the Native.
It receives three parameters:

* methodName: the name of the handler registered in the Native.
* params: the parameters to send to the Native. It needs to be a JSON string.
* callback: the callback function to handle the response from the Native. It receives a JSON string
as the parameter. Pass null if no callback is needed.

```javascript
window.kmpJsBridge.callNative = function (methodName, params, callback) {}

window.kmpJsBridge.callNative("Greet",JSON.stringify({message:"Hello"}),
function (data) {
document.getElementById("subtitle").innerText = data;
console.log("Greet from Native: " + data);
}
);
```

## WebSettings
Starting from version 1.3.0, this library allows users to customize web settings.
There are some common web settings that can be shared across different platforms, such as isJavaScriptEnabled and userAgent.

Starting from version 1.3.0, this library allows users to customize web settings.
There are some common web settings that can be shared across different platforms, such as
isJavaScriptEnabled and userAgent.

```kotlin
class WebSettings {
var isJavaScriptEnabled = true
var isJavaScriptEnabled = true

var customUserAgentString: String? = null
var customUserAgentString: String? = null

/**
/**
* Android platform specific settings
*/
val androidWebSettings = PlatformWebSettings.AndroidWebSettings()
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
kotlin("multiplatform").apply(false)
kotlin("plugin.serialization").apply(false)
id("com.android.application").apply(false)
id("com.android.library").apply(false)
id("org.jetbrains.compose").apply(false)
id("org.jetbrains.dokka")
id("com.vanniktech.maven.publish") version "0.25.3" apply false
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
id("org.jetbrains.kotlin.plugin.atomicfu") version "1.9.20"
}

subprojects {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ compose.version=1.5.10

GROUP=io.github.kevinnzou
POM_ARTIFACT_ID=compose-webview-multiplatform
VERSION_NAME=1.7.8
VERSION_NAME=1.8.0-SNAPSHOT

POM_NAME=Compose WebView Multiplatform
POM_INCEPTION_YEAR=2023
Expand Down
8 changes: 8 additions & 0 deletions sample/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ plugins {
kotlin("multiplatform")
id("com.android.library")
id("org.jetbrains.compose")
kotlin("plugin.serialization")
id("org.jetbrains.kotlin.plugin.atomicfu")
}

@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
Expand Down Expand Up @@ -29,6 +31,7 @@ kotlin {
}

sourceSets {
val coroutines = "1.7.3"
val commonMain by getting {
dependencies {
implementation(compose.runtime)
Expand All @@ -38,17 +41,22 @@ kotlin {
implementation(compose.components.resources)
implementation("co.touchlab:kermit:2.0.0-RC5")
api(project(":webview"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("org.jetbrains.kotlinx:atomicfu:0.21.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
val androidMain by getting {
dependencies {
api("androidx.activity:activity-compose:1.7.2")
api("androidx.appcompat:appcompat:1.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines")
}
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.common)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$coroutines")
}
}
val commonTest by getting {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,81 +14,57 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import co.touchlab.kermit.Logger
import com.kevinnzou.sample.eventbus.FlowEventBus
import com.kevinnzou.sample.eventbus.NavigationEvent
import com.kevinnzou.sample.jsbridge.GreetJsMessageHandler
import com.kevinnzou.sample.res.HtmlRes
import com.multiplatform.webview.jsbridge.WebViewJsBridge
import com.multiplatform.webview.jsbridge.rememberWebViewJsBridge
import com.multiplatform.webview.util.KLogSeverity
import com.multiplatform.webview.web.WebView
import com.multiplatform.webview.web.WebViewState
import com.multiplatform.webview.web.rememberWebViewNavigator
import com.multiplatform.webview.web.rememberWebViewStateWithHTMLData
import kotlinx.coroutines.flow.filter

/**
* Created By Kevin Zou On 2023/9/8
*/
@Composable
internal fun BasicWebViewWithHTMLSample() {
val html =
"""
<html>
<head>
<title>Compose WebView Multiplatform</title>
<style>
body {
background-color: e0e8f0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
margin: 0;
}
h1, h2 {
text-align: center;
color: ffffff;
}
</style>
</head>
<body>
<script type="text/javascript">
function callJS() {
return 'Response from JS';
}
</script>
<h1>Compose WebView Multiplatform</h1>
<h2 id="subtitle">Basic Html Test</h2>
</body>
</html>
""".trimIndent()
val html = HtmlRes.html
// val webViewState = rememberWebViewStateWithHTMLFile(
// fileName = "index.html",
// )
val webViewState = rememberWebViewStateWithHTMLData(html)
LaunchedEffect(Unit) {
webViewState.webSettings.apply {
zoomLevel = 1.0
isJavaScriptEnabled = true
logSeverity = KLogSeverity.Debug
allowFileAccessFromFileURLs = true
allowUniversalAccessFromFileURLs = true
androidWebSettings.apply {
isAlgorithmicDarkeningAllowed = true
safeBrowsingEnabled = true
allowFileAccess = true
}
}
}
val webViewNavigator = rememberWebViewNavigator()
val jsBridge = rememberWebViewJsBridge(webViewNavigator)
var jsRes by mutableStateOf("Evaluate JavaScript")
LaunchedEffect(Unit) {
initWebView(webViewState)
initJsBridge(jsBridge)
}
MaterialTheme {
Box(Modifier.fillMaxSize()) {
WebView(
state = webViewState,
modifier = Modifier.fillMaxSize(),
captureBackPresses = false,
navigator = webViewNavigator,
webViewJsBridge = jsBridge,
)
Button(
onClick = {
webViewNavigator.evaluateJavaScript(
"""
document.getElementById("subtitle").innerText = "Hello from KMM!";
window.kmpJsBridge.callNative("Greet",JSON.stringify({message: "Hello"}),
function (data) {
document.getElementById("subtitle").innerText = data;
console.log("Greet from Native: " + data);
}
);
callJS();
""".trimIndent(),
) {
Expand All @@ -102,3 +78,32 @@ internal fun BasicWebViewWithHTMLSample() {
}
}
}

fun initWebView(webViewState: WebViewState) {
webViewState.webSettings.apply {
zoomLevel = 1.0
isJavaScriptEnabled = true
logSeverity = KLogSeverity.Debug
allowFileAccessFromFileURLs = true
allowUniversalAccessFromFileURLs = true
androidWebSettings.apply {
isAlgorithmicDarkeningAllowed = true
safeBrowsingEnabled = true
allowFileAccess = true
}
}
}

suspend fun initJsBridge(webViewJsBridge: WebViewJsBridge) {
webViewJsBridge.register(GreetJsMessageHandler())
// EventBus.observe<NavigationEvent> {
// Logger.d {
// "Received NavigationEvent"
// }
// }
FlowEventBus.events.filter { it is NavigationEvent }.collect {
Logger.d {
"Received NavigationEvent"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.kevinnzou.sample.eventbus

import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.getAndUpdate
import kotlin.reflect.KClass

/**
* Created By Kevin Zou On 2023/12/15
*/

typealias Observer = (Any) -> Unit

object EventBus {
private val observers = atomic(mutableMapOf<KClass<out Any>, List<Observer>>())

fun <T : Any> observe(
clazz: KClass<T>,
obs: (T) -> Unit,
) {
if (!observers.value.containsKey(clazz)) {
observers.getAndUpdate { cur ->
cur.toMutableMap().also { upd ->
upd[clazz] = listOf(obs as Observer)
}
}
} else {
observers.getAndUpdate { cur ->
cur.toMutableMap().also { upd ->
upd[clazz] = upd[clazz]!! + listOf(obs as Observer)
}
}
}
}

inline fun <reified T : Any> observe(noinline obs: (T) -> Unit) {
observe(T::class, obs)
}

fun <T : Any> removeObserver(
clazz: KClass<T>,
obs: (T) -> Unit,
) {
observers.getAndUpdate { cur ->
cur.toMutableMap().also { upd ->
upd.remove(clazz)
}
}
}

fun <T : Any> post(
clazz: KClass<T>,
event: T,
) {
observers.value[clazz]?.forEach { it.invoke(event) }
}

inline fun <reified T : Any> post(event: T) {
post(T::class, event)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.kevinnzou.sample.eventbus

import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow

/**
* Created By Kevin Zou On 2023/12/16
*/
object FlowEventBus {
private val mEvents = MutableSharedFlow<IEvent>()
val events = mEvents.asSharedFlow()

suspend fun publishEvent(event: IEvent) {
mEvents.emit(event)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kevinnzou.sample.eventbus

/**
* Created By Kevin Zou On 2023/12/16
*/
interface IEvent
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kevinnzou.sample.eventbus

/**
* Created By Kevin Zou On 2023/12/15
*/
class NavigationEvent : IEvent
Loading

0 comments on commit c0bd75b

Please sign in to comment.