Creating a queue for the evaluateJavascript function on a WebView

I have a hybrid app; some of my Activities use a WebView to display web content. The web app that I show in the WebView has a JS interface that lets me send commands to the web app to navigate different places or do other things.

For example, if I need my web app to navigate to the "user profile" page, I execute a command like:

class SomeActivity: AppCompatActivity {
   ...
   webView.evaluateJavascript("navigateTo(\"userprofile\")")
   ...
}

Then, I get a response via the JS interface, and the app reacts accordingly.

I introduced a JS queue to improve performance, so the JS commands are executed sequentially. Instead of calling the evaluateJavascript() function directly on the WebView, I've created a custom WebView component with this JS queue set as a property.

class SomeActivity: AppCompatActivity {
   ...
   webView.jsQueue.queueEvaluateJavascript("navigateTo(\"userprofile\")")
   ...
}

Now I would like to add a new behaviour on top of that, which is being able to pre-process the commands within the queue. What I mean by pre-processing is that if I ever queue commands of the same "type", like:

class SomeActivity: AppCompatActivity {
   ...
   webView.jsQueue.queueEvaluateJavascript("navigateTo(\"userprofile\")")
   webView.jsQueue.queueEvaluateJavascript("navigateTo(\"about-me\")")
   webView.jsQueue.queueEvaluateJavascript("navigateTo(\"user-list\")")
   ...
}

What I would like to happen is that the queue is smart enough to ditch those two first "navigate" commands - "navigateTo(\"userprofile\")" and "navigateTo(\"about-me\")" - because I don't want my WebView to navigate to those two places just to finally navigate to "navigateTo(\"user-list\")".

The implementation of this JS queue looks like this:

class JsQueue(
    private val webView: WebView,
    private val scope: CoroutineScope
) {
    
    init {
        scope.launch { 
            for (jsScript in jsChannel) {
                runJs(jsScript)
            }
        }
    }

    private val jsChannel = Channel<String>(BUFFERED)

    fun queueEvaluateJavascript(script: String) {
        runBlocking {
            jsChannel.send(script)
        }
    }

    suspend fun runJs(script: String) = suspendCoroutine<String> { cont ->
        webView.evaluateJavascript(script) { result ->
            cont.resume(result)
        }
    }
}
  • How can I pre-process the js commands in the Channel<String> so I ditch duplicated js commands?
  • Also, sometimes my WebView will become invisible, and I want to pause the queue when that happens. I wonder if there's any way to programmatically pause a Channel?

Edit #1

Also, sometimes my WebView will become invisible, and I want to pause the queue when that happens. I wonder if there's any way to programmatically pause a Channel?

I've tried using this PausableDispatcher implementation, and it seems to be doing the trick.


Solution 1:

All of the command examples you gave follow a specific pattern: they're all functions. We can use this to our advantage!

First, let's create some terminology.

navigateTo() is a function (of course!). And lets call the navigateTo part of the function a type.

An example of some types are:

console.log() => `console.log`,
gotoUrl(url) => `gotoUrl`.

I just made this terminology up. But it will help you understand the logic.

Now, what we need to do is look at the array of commands, understand it's type, and check if any other commands have the same type. If they do, they need to be excluded from the queue before the queue is executed.

Easy!

I've written a sample code that you can integrate with your script:

// Example array of commands for demonstration.
let commands = [
    'navigateTo("a")',
    'navigateTo("b")',
    'navigateTo("c")',
];


/** A list of non-duplicate types*/
let types = [];
/** A list of non-duplicate commands */
let newCommands = [];

// Reverse the array because the most important commands start from the end of array.
for(let command of commands.reverse()){
    let type = command.slice(0, command.indexOf('('));
    // Determine if type already exists
    let alreadyExists = false;
    for(let commandType of types){
      if(type == commandType){
        alreadyExists = true;
        break;
      }
    }
  
    if(alreadyExists)
      // Type already exists. Do not add to command list.
      continue;
    
     // This type & command does not exist.
     // Update command & type arrays
     types.push(type);
     newCommands.push(command)
}

// New Commands
console.log("Commands: ", newCommands);
// If you want to keep same queue order without duplicates:
console.log("Commands: ", newCommands.reverse())

Let me know if I missed the mark answering this question. Otherwise, cheers to a great queue system!