Cordova: How to write native plugins that can repeatedly invoke a Javascript callback?

10,103

I think, you can use a pluginResult with the keepCallback property set to true.

PluginResult result = new PluginResult(PluginResult.Status.OK, "YOUR_MESSAGE");
// PluginResult result = new PluginResult(PluginResult.Status.ERROR, "YOUR_ERROR_MESSAGE");
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);

You should be able to invoke the callback several times this way.

Share:
10,103
n.abing
Author by

n.abing

Updated on June 05, 2022

Comments

  • n.abing
    n.abing almost 2 years

    When developing Cordova plugins, all of the tutorials I have found go something like this:

    File: AwesomePlugin.js

    var AwesomePlugin = {
      kungfuGripAction = function(target, successCallback, failureCallback) {
        return cordova.exec(
          successCallback,
          failureCallback,
          'AwesomePluginClass',
          'kungfuGripAction',
          [target]
        );
      }
    };
    
    module.exports = AwesomePlugin;
    

    File: AwesomePluginClass.java

    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if (ACTION_KUNGFU_GRIP.equals(action)) {
            JSONObject target = args.getJSONObject(0);
            if (gripTarget(target)) {
                callbackContext.success("Target successfully gripped.");
                return true;
            } else {
                callbackContext.error("Could not grip target.");
                return false;
            }
        }
    
        Log.d(LOG_TAG, "INVALID ACTION! " + action);
        callbackContext.error("Invalid action: " + action);
        return false;
    }
    

    File: clientCode.js

    AwesomePlugin.kungfuGripAction(cobraEnemy, function(ok) { }, function(err) { });
    

    In the above code, the callbacks can only be called once and are then disposed. If you attempt to call the .success() or .error() method of the callback context object, it will not work and you will get a log message:

    Attempted to send a second callback for ID: AwesomePlugin2982699494<BR>W/CordovaPlugin(976) Result was: "Target successfully gripped."
    

    It seems like it is not possible to write a method with a callback that can be called repeatedly seeing as .success() and .error() are the only documented ways to invoke a callback from within native plugin code. While this is mostly what we want, there are times when we want to have the plugin execute a callback repeatedly. For example:

    AwesomePlugin.kungfuGripAction(cobraEnemy, function(ok) {
      // After successful grip, punch repeatedly and update life meter.
      AwesomePlugin.punchRepeatedly(cobraEnemy, function(hits) {
        updateLifeMeter(cobraEnemy, hits);
      }, function(err) { });
    }, function(err) { });
    

    AwesomePlugin.punchRepeatedly() above will execute repeatedly (maybe in a separate thread) and call function(hits) with each successful execution. If implemented in the de-facto way (using single-use callbacks), you have to either use a loop (which is bad as it is non-async) or tail-call AwesomePlugin.punchRepeatedly() in the callback (error-prone).

    What would be the correct way to implement punchRepeatedly() in native code so that it is able register the callback once and then execute it repeatedly?

  • n.abing
    n.abing over 9 years
    Thanks, this works. The PluginResult.setKeepCallback(true) was the key to this.
  • n.abing
    n.abing about 8 years
    You should generally avoid modifying framework code directly. When you upgrade your Cordova framework your changes are lost. You should also not need to change the message in a plugin result as you already set that when you create a new instance and then send it to the callback. The only time you might want to do this is if you are re-using a single instance of PluginResult in your plugin, which is also a bad idea.
  • lwiu
    lwiu about 8 years
    You are right.But I mean to reuse the single instance of Pluginresult by setting setKeepCallback (true),so what's the better way to send a different message each time?In swift language I can use an extension
  • n.abing
    n.abing about 8 years
    What do you mean? You don't have to reuse a single instance of PluginResult. You can instantiate a new PluginResult each time you need to send back a result to the callback and use a different message if you want. Using result.keepCallback(true) only tells callbackContext.sendPluginResult() to not mark the callback as "used" so that you can reuse the callbackContext in sending results again.
  • lwiu
    lwiu almost 8 years
    I originally tried to do as you said,but it would only receive the first callback. I/chromium: [INFO:CONSOLE(268)] "First success", source: file:///android_asset/www/build/js/app.bundle.js (268) W/IInputConnectionWrapper: showStatusIcon on inactive InputConnection D/CordovaInterfaceImpl: Sending activity result to plugin W/CordovaPlugin: Attempted to send a second callback for ID: Bluetooth435896053 Result was: "Third success"
  • n.abing
    n.abing almost 8 years
    It's hard to tell what you are doing wrong without any context or sample of your code. But that should not happen if you create a new instance of the PluginResult each time you send it via CallbackContext.sendPluginResult(). You probably did not call PluginResult.keepCallback(true). As I said above, keepCallback(true) only instructs sendPluginResult() to not mark the callback as "used" so that you can keep reusing it. You can look at the code for sendPluginResults() here: github.com/apache/cordova-android/blob/master/framework/src/‌​org/…
  • lwiu
    lwiu almost 8 years
    I got what you said !Did you mean each instance of Pluginresult should setKeepCallback(true)?!Thank a lot 👍
  • n.abing
    n.abing almost 8 years
    Yes, after you create the new instance of PluginResult you should use setKeepCallback(true) before using it in sendPluginResult(), otherwise you will get the error you described above.
  • tshm001
    tshm001 almost 6 years
    @n.abing doesn't this trigger the success callback multiple times by passing status.ok each time you're sending a new callback?
  • n.abing
    n.abing almost 6 years
    @TimothyShamilov no it does not do that. It's been a long time since I've written anything in Cordova but IIRC result.setKeepCallback(true) only ensures that the callback function is not destroyed when the result is destroyed. When in doubt, use the source :)