首页 > 解决方案 > 使用 Kotlin 协程按顺序运行 evaluateJavascript() 函数

问题描述

我正在做一个宠物项目,我正在尝试使用WebView. 我在其中运行的 Web 平台通过对象WebView向 WebView/App 发送事件。@JavascriptInterface我还可以通过使用该函数在 Web 平台上运行一组 javascript 函数来命令Web 导航。WebViewevaluateJavascript(String, (String) -> Unit)

我现在想要实现的是,我通过函数执行的这些命令evaluateJavascript(String, (String) -> Unit)是按顺序运行的。我可能会同时从许多不同的地方执行这些命令,所以我希望它们运行,等待evaluateJavascript()函数的回调被调用,然后执行队列中的下一个命令。

这就是我在我的自定义WebView类中所拥有的:

    val scriptQueue = mutableListOf<String>()
    
    fun queueEvaluateJavascript(script: String) {
        if (webViewIsLoading) {
            scriptQueue.add(script)
        } else {
            scriptQueue.add(script)
            runScriptQueue()
        }
    }
    
    fun runScriptQueue() {
        for (script in scriptQueue) {
            evaluateJavascript(script, { })
        }
        scriptQueue.clear()
    }

如您所见,这是一种超级基本的方法,我并没有真正考虑evaluateJavascript()回调。理想情况下,我想找到一种方法来平面映射每个evaluateJavascript()调用,以便我们一个接一个地执行,但等待回调通过。

使用 RxJava,我想我会创建一个 Observable,然后让evaluateJavascript()回调触发订阅者的onNext(). 因为,我正在使用 Kotlin Coroutines,所以我想用 Coroutines 做一些事情,所以我可以对这些evaulateJavascript()调用进行排队。但我不是 100% 确定这里的等价物是什么。

标签: androidkotlinandroid-webviewkotlin-coroutines

解决方案


这将是使用协程处理的一个很好的问题。

将基于回调的 API 转换为挂起函数的常用方法如下:

suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
    evaluateJavascript(script) { result ->
        cont.resume(result)
    }
}

然后,您可以将它与Channel(用作队列)和处理此通道的协程结合使用:

class MyWebView(context: Context) : WebView(context) {
    
    private val jsQueue = Channel<String>(BUFFERED)

    fun startJsProcessingLoopIn(scope: CoroutineScope) {
        scope.launch { 
            for (script in jsQueue) {
                evaluateJs(script)
            }
        }
    }

    // you could also make this function non-suspend if necessary by calling
    // sendBlocking (or trySend depending on coroutines version)
    suspend fun queueEvaluateJavascript(script: String) {
        jsQueue.send(script)
    }

    private suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
        evaluateJavascript(script) { result ->
            cont.resume(result)
        }
    }
}

或者,您可以创建自己的协程范围,并确保将其与 webview 的某种生命周期联系起来(我不熟悉,WebView所以我会让您判断哪种方法是正确的):

class MyWebView2(context: Context) : WebView(context) {

    // you can even further customize the exact thread pool used here 
    // by providing a particular dispatcher
    private val jsProcessingScope = CoroutineScope(CoroutineName("js-processing"))

    private val jsQueue = Channel<String>(BUFFERED)

    // this starts the loop right away but you can also put this in a method 
    // to start it at a more appropriate moment
    init {
        jsProcessingScope.launch {
            for (script in jsQueue) {
                evaluateJs(script)
            }
        }
    }

    // you could also make this function non-suspend if necessary by calling
    // sendBlocking (or trySend depending on coroutines version)
    suspend fun queueEvaluateJavascript(script: String) {
        jsQueue.send(script)
    }

    private suspend fun evaluateJs(script: String) = suspendCoroutine<String> { cont ->
        evaluateJavascript(script) { result ->
            cont.resume(result)
        }
    }

    fun someCloseOrDisposeCallback() {
        jsProcessingScope.cancel()
    }
}

推荐阅读