Node v6
Starting with v0.19 Astro must be used with Node v6+!
Menu

Writing Astro Plugins

Astro provides the idea of a "plugin", which is a combination of native code and JavaScript running in the "app" that allows you to initialize and co-ordinate state changes in the native portion of the UI from JavaScript.

A typical plugin consists of three pieces:

  1. A JavaScript class (mostly a proxy object)
  2. An iOS class that inherits from Astro.Plugin
  3. An Android class that inherits from com.mobify.astro.AstroPlugin

Both native plugin implementations will conform to the same set of methods and events that the JavaScript plugin class uses. The core concepts for building an Astro plugin is the same whether you are working on iOS or Android, but the class names and conventions differ slightly by platform.

An Astro plugin communicates between the JavaScript plugin instance and native plugin instance using the "bridge". This is a message bus that can deliver messages from JavaScript to native and back.

JavaScript

The first step in building an Astro plugin is to define the API that the JavaScript class will expose. Astro plugins are built as JavaScript modules that export a single constructor function (class). Plugins should attach a property to the constructor function named 'pluginName' that is the same as the constructor function (eg. SharingPlugin.pluginName = 'SharingPlugin';). This property provides the name that the native side will use to resolve the associated native plugin class (resolution differs between iOS and Android... the simplest way to keep everything correct is to use the same class name for all pieces of a plugin: JavaScript, iOS, and Android).

Initialization

All Astro Plugins must implement an init method. This method can take parameters if the plugin has dependencies. Typically the init method will delegate directly to the PluginManager.createPlugin method to create the native plugin counterpart. An example init:

MyPlugin.init = function(options, callback) {
    return PluginManager.createPlugin(MyPlugin, options, callback)
}

In some cases a plugin is purely JavaScript. This could be because the plugin simply composes multiple plugins or it is a feature that can be implemented purely in JavaScript (maybe access to local storage). In this case the init method can just delegate directly to the constructor function. However, because init must return a Promise, you will need to wrap the constructed object in a Promise manually. Eg.

const MyPlugin = function() {}

MyPlugin.init = function(options, callback) {
    return Promise.resolve(new MyPlugin())
}

Methods

Plugin methods should be attached to the constructor function prototype. If the method is a simple proxy for a native plugin method, use the Astro.nativeRpcMethod function which will generate a method that proxies to the native method for you.

Full example

import Astro from 'astro-full'
import PluginManager from 'plugin-manager'

/**
* Constructor
*/
const MyPlugin = function() {}

/**
* Defines the plugin name, which is necessary for
* initialization of the plugin on the native side.
*/
MyPlugin.pluginName = 'MyPlugin'

MyPlugin.init = function(options, callback) {
    return PluginManager.createPlugin(MyPlugin, options, callback)
}

/**
* Set the image path of the image to display in this plugin.
* Note: This demonstrates using the proxy method generator for a
*       native plugin method.
*/
MyPlugin.prototype.setImagePath = Astro.nativeRpcMethod('setImagePath', ['path'])

/**
* A simple delay method to demonstrate a pure-JavaScript plugin method.
*/
MyPlugin.prototype.delay = function(timeout, callback) {
    setTimeout(callback, timeout)
}

export default MyPlugin

Android

Begin by creating a new class within your project. This class must inherit from com.mobify.astro.AstroPlugin.

Constructor

Create a constructor if your plugin needs to do some initialization when it's created. This includes setting up any UI components and adding them to the plugin's layout. For example:

public MyPlugin(AstroActivity activity, PluginResolver pluginResolver) {
    super(activity, pluginResolver);

    // Other initialization...
}

RPC Methods

Add any other methods that you want to expose through the JavaScript side of the plugin as normal methods. These are called RPC methods and can accept parameters that conform to the types documented here.

To expose these methods to the JavaScript counterpart of the plugin, annotate each RPC method with the @RpcMethod annotation. Pass the method name and list of parameter names to the annotation.

@RpcMethod(methodName = 'setImagePath', parameterNames = {"path" /*, other parameter names... */ })
public void setImagePath(String path)
{
    // ...
}

Asynchronous Methods

Using the @RpcMethod annotation marks the method as a synchronous method. This means that the bridge will send the return value from the method back to the JavaScript caller when the method finishes. It is possible to make the method asynchronous by using the special @AsyncRpcMethod annotation. The usage is the same, except that your Java function must take an RpcResponse object as the first parameter (and then any other parameters you function accepts). Because the method is now asynchronous, the bridge will ignore the return value from the function. To communicate the return value from the async method, use one of the RpcResponse object methods setResult or setError and then call send().

Plugin UI (Views)

Android plugin classes can provide a UI component by implementing the getView method. There are no requirements on what kind of view you export as long as it inherits from the standard Android android.view.View class. A typical pattern is to create a container view in the plugin constructor, configure it, add any subviews, and then return this container view from getView.

Note that Android uses "dp" instead of "pixels" for UI coordinates - see here for more information.

Registration

Once you have the Android plugin class created you need to register it with Astro. This is the final link needed so that Android can find the correct plugin class when a JavaScript plugin makes a method call across the bridge.

Add these registration calls in your MainActivity that inherits from AstroActivity. The PluginManager (which is an instance that you have access to within the MainActivity's OnCreate method) has a register method that takes the plugin class to be registered.

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Register plugins.
    // ...
    pluginManager.register(MyPlugin.class);
}

Error handling

The Astro Android SDK does not handle exceptions thrown from your plugin (ie. if your plugin does something that results in an exception being thrown, your app will crash). There may be times when you want to communicate an error from your plugin to app.js to handled there. There are two ways to do this depending on if your method is synchronous or asynchronous.

Synchronous Methods

If the method is marked as synchronous (@RpcMethod), you will need to create a custom AstroException subclass and throw it (eg. DrawerNotInitializedException). The Astro SDK will catch any exception that inherits from AstroException and return the message string from that exception as an error to app.js.

Asynchronous Methods

If the method is marked as asynchronous (@AsyncRpcMethod), just use the RpcResponse object passed to your method to communicate the error by calling .setError(String) and then .send().

iOS

The process of building the iOS portion of an Astro plugin is similar to Android. Begin by creating a new class in your iOS project and inheriting from Astro.Plugin.

Initializer

Create an initializer (constructor) for the class. This initializer must call the required init that Astro.Plugin defines. The following example provides a basic initializer that provides a good starting point:

public required init(address: MessageAddress, messageBus: MessageBus, pluginResolver: PluginResolver, options: JSONObject?) {
    super.init(address: address, messageBus: messageBus, pluginResolver: pluginResolver, options: options)

    // Other initialization...
}

RPC Methods

An iOS plugin class can export methods to its JavaScript counterpart similar to how Android does it. One notable difference is that Swift does not provide usable runtime reflection capabilities and so we have to do a bit more manual work to hook up the RPC methods.

Begin by writing the function as a regular instance function. By convention we annotate all RPC methods in iOS with an "@RpcMethod" comment as follows:

/// @RpcMethod
func setImagePath(path: String, respond: RPCMethodCallback) {
    // ...
}

All RPC methods must take a respond: RPCMethodCallback parameter. By convention this parameter is always the last parameter. If your method does not take any parameters, it should still take the single respond parameter.

The iOS SDK never uses the return value from an RPC method. Instead the return value is always communicated across the bridge using the respond object. To return a result, call respond(.result({object})). You can accept or return any type listed in the documentation here.

Asynchronous methods

A method can be marked as asynchronous when it is registered using the registerAsyncRPCMethod() method (see below). Other than that an asynchronous method is the same as a regular RPC method in iOS (Swift).

RPC Method Registration

RPC methods must be registered in iOS. Each RPC method is registered within the init method of the plugin by calling addRPCMethodShim (for synchronous methods) or addAsyncRPCMethodShim (for asynchronous methods). Method parameters are usually unpacked in the RPC method registration by using helper methods found in the MethodShimUtils static class. Use getArg for RPC method parameters that must be provided. The helper method will also validate that the parameter is of the type you specified (based on inferring the type you are assigning to). If the parameter is not supplied the getArg helper method will automatically respond with an error code and return nil. You can use if let optional binding, which will just skip calling your RPC method if getArg returns nil. Eg.

self.addRPCMethodShim("setMainViewPlugin") { params, respond in
    ////////// This will be autogenerated at some point //////////
    if let address: MessageAddress = MethodShimUtils.getArg(params, key: "address", respond: respond) {
        self.setMainViewPlugin(address, respond: respond)
    }
    /////////////////////////////////////////////////////////////
}

For optional arguments you can use the getOptionalArg on MethodShimUtils. This method may return nil if the parameter wasn't supplied. It will still verify the parameter type is correct if it has been supplied (using the same method that getArg uses (type inference)). Eg.

if let options: JSONObject? = MethodShimUtils.getOptionalArg(params, key: "options", respond: respond) {
    ////////// This will be autogenerated at some point //////////
    self.methodWithOptions(options, respond: respond)
    /////////////////////////////////////////////////////////////
}

Plugin UI (Views)

iOS plugin classes that want to export a UI component must conform to the ViewPlugin protocol. The protocol exposes a single UIViewController property. So to export a UI from your plugin, add the property and then compose the plugin UI in that viewController's .view property. For simple plugin views it is common for the type of the viewController to be a base UIViewController as the plugin class itself can coordinate the UIViewController's view.

For more complex UI's it might make sense to create a custom UIViewController subclass and return that in your plugin's viewController property. In this case your plugin would coordinate with your custom UIViewController: the plugin arbitrating communication from JS and managing state and the custom view controller managing the UI, animations, etc. for the plugin. Apply solid engineering practices as you build your plugin and split it into separate classes if it starts to take on too many responsibilities.

Note that iOS uses "points" instead of "pixels" for UI coordinates - see here for more information.

Registration

As with Android plugin classes, iOS plugins must be registered before they are visible to the JavaScript counterpart. In the AppDelegate of your Astro iOS project add the registration calls in the pluginRegistrations block parameter of the AstroViewController. The block receives a parameter with a method you can use to do the registration. Eg.

astroViewController = AstroViewController(appJSURL: URL(string: "app.js")!, launchOptions: launchOptions) { pluginRegistrar in
    pluginRegistrar.registerPlugin(name: "MyPlugin", type: MyPlugin.self)
}

Low-level details

From the JavaScript side, RPC calls can be made by calling Astro.postMessage(address, message, callback) with an address as given by the Plugin Manager message of the form:

{
    method: '<string: method name as annotated>',
    params: {
        <key: value pairs of parameters, where keys are the annotated parameter names>
    }
}

When the RPC call is ready to return its value, it will invoke Astro.receiveMessage(data) with the response RPC message, and the callback passed to Astro.postMessage() for this call will be called with a single argument that is the result of the RPC call.