Home > other >  Run evaluateJavascript() function sequentially using Kotlin Corutines
Run evaluateJavascript() function sequentially using Kotlin Corutines

Time:11-06

I'm working on a pet project where I'm trying to create a hybrid app using a WebView. The web platform that I run in the WebView sends events to the WebView/App through a @JavascriptInterface object. I can also command the web navigation by running a set of javascript functions against the web platform via the WebView using the evaluateJavascript(String, (String) -> Unit) function.

What I'm trying to achieve right now is that these commands that I execute through the evaluateJavascript(String, (String) -> Unit) function run sequentially. I might execute these commands from many different places at the same time, so I want them to run, wait for the callback from the evaluateJavascript() function to get called, and then execute the next command in the queue.

This is what I have in my custom WebView class:

    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()
    }

As you can see this is a super basic approach, and I don't really account for the evaluateJavascript() callback. Ideally, I'd like to find a way to flat map each of this evaluateJavascript() calls so we execute one after another, but waiting for the callback to go through.

With RxJava I think I'd create an Observable and then have the evaluateJavascript() callback trigger the subscriber's onNext(). Since, I'm using Kotlin Coroutines I wanted to do something with Coroutines, so I can queue these evaulateJavascript() calls. But I'm not 100% sure what would be the equivalent here.

CodePudding user response:

That would be a nice problem to approach with coroutines.

The usual way to convert callback based APIs to suspend functions is the following:

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

You can then use that in combination maybe with a Channel (to serve as a queue) and a coroutine that processes this 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)
        }
    }
}

Alternatively you can create your own coroutine scope and make sure to tie it with some sort of lifecycle of your webview (I'm not familiar with WebView so I'll let you judge which kind of method is correct):

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()
    }
}
  • Related