Robopupu.Plugin API

Plugin API provides a unique mechanism for inter-component communication. Communication is based on plugin interface method invocations that are delegated and dispatched via a plugin bus. Inter-component communication can be one-to-one (in reference mode) or one-to-many (in broadcast mode). An overview of the plugin bus communication is illustrated in the following diagram. All the components shown in the diagram are plugin components which are plugged to the plugin bus.

pluginbus_overview

The Plugin API consists of the following key elements:

  • Plug: An instance field annotated using annotation @Plug. A value assigned to a plug field is a reference to an instance of class PluginInvoker.
  • PluginInvoker: An object created and managed by a PluginBus. A PluginInvoker contains zero or more references to plugin components. In reference mode, it has exactly one reference, and in broadcast mode, zero or more references. In latter mode, a component can act as a kind of publisher that posts events (as method invocations) to multiple subscriber component. Note that the type of a  plug field is the same plugin interface that both the assigned PluginInvoker and the plugin component implement. So, the main function of a PluginInvoker is just to delegate method invocations to one or more components for which it has references.
  • PluginBus: A manager object that provides an API for registering and unregistering plugin components for and from inter-component communication. Plugin component registration is based both on the plug fields of the plugin components and on the plugin interfaces they implement.

Inter-component communication via a plugin bus has a good performance and is fully debuggable because there is no reflection involved. Using a communication mechanism based on invoking interface methods eliminates the need for writing special event objects like is done in event bus based libraries such as Square’s Otto and Greenrobot’s EventBus.

PluginBus API provides also an additional way to implement dependency injection (DI). As described later on, when a plugin component is plugged to a PluginBus, the fields annotated with @Plug are injected to have references to appropriate PluginInvoker instances. Robopupu utilises this kind of dependency injection to attach presenters and views objects to each other via plug fields as described in Robopupu.MVP API. In fact, almost all inter-component communication in an application using Robopupu APIs is intended to be based on using plugin bus. But of course, nothing prevents the developers to use the mentioned event bus libraries as well.

Key API Classes

The key classes implementing the Plugin API are depicted in the following class diagram. Classes with white background are the framework classes. Classes with light yellow background are code generated by the Robopupu Compiler. Classes with light blue background represent examples of classes that are written by a developer.

pluginbus_key_classes

  • PluginBus: A manager object for plugin components. Provides a method for registering a plugin component to a PluginBus, and naturally a method for unregistering  the plugged component from the PluginBus.
  • PlugInvoker: An object that implements some plugin interface for delegating method invocations to a plugin component that implements the very same interface. Class PlugInvoker is actually  an abstract base class for concrete implementations which are generated by the annotation processor in the Robopupu Compiler. Code generation eliminates the need to use reflection API and to write boiler plate code.
  • Plugger: A utility object that is used for both registering a plugin component to a PluginBus and for unregistering it from PluginBus.
  • Plug: An annotation for declaring fields for which PlugInvoker instances are injected. The annotation processor in Robopupu Compiler uses the @Plug annotation for triggering  code generation of Plugger implementations.
  • PlugInterface: An annotation for declaring plug interfaces. The annotation processor in Robopupu Compiler uses @PlugInterface annotation for triggering code generation of both  Plugger and  PlugInvoker implementations.
  • PluginComponent: An interface for plugin components that are aware of being plugged into a PluginBus and that can receive events from it.
  • PluginStateComponent: Extends PluginComponent interface with methods related to lifecycle events and PluginState.
  • PluginState: An object that represents the current state of a PluginStateComponent.
  • AbstractPluginComponent: An abstract base class provided for implementing PluginComponents.
  • AbstractPluginStateComponent: An abstract base class provided for implementing PluginStateComponents.

Component TimerManagerImpl implements TimerManager interface to provide timer related services. The implementation of them is discussed in the following section. Classes TimerManager_PlugInvoker and TimerManagerImpl_Plugger are helper classes that are code generated by the Robopupu Compiler in order to avoid writing boiler plate code.

Implementing a Plugin Component

PluginComponents can be easily implemented by defining one or more interfaces(s) that are annotated using @PlugInterface. An annotated plugin interface defines the services that the implementing PluginComponent provides for other plugin components via a plugin bus. TimerManager interface is an example of such service interface: A plugin component that implements TimerManager interface provides timer related services for other plugin components:


import com.robopupu.api.component.Manager;
import com.robopupu.api.plugin.PlugInterface;

@PlugInterface
public interface TimerManager extends Manager {

    /**
     * Creates and starts a timer that expires after the specified delay.
     * @param delay delay in milliseconds before the timer expires.
     * @return A {@link TimerHandle} that can be used to cancel the timer.
     */
    TimerHandle createTimer(Callback callback, long delay);

    interface Callback {
        void timeout(TimerHandle handle);
    }
}

TimerManager interface is annotated with @PlugInterface. This annotation triggers the annotation processor for Robopupu.Plugin API to generate a class with name TimerManager_PlugInvoker. A PluginBus uses this class when some plugin component that either implements the interface TimerManager or has a @Plug annotated field of the type TimerManager is registered to PluginBus .

Plugin component TimerManagerImpl which implements TimerManager interface is extended from AbstractManager class which in turn is extended from AbstractPluginComponent. Note that the TimerManagerImpl is annotated with @Plugin. This is required so that the annotation processor in Robopupu Compiler will be triggered to generate an implementation of Plugger that is used for registering instances of TimerManagerImpl to PluginBus. The annotation processor  generates a class with name TimerManagerImpl_Plugger.


package com.robopupu.component;
...
@Plugin
public class TimerManagerImpl extends AbstractManager implements TimerManager {
    private final HashMap<Long, TimerHandle> mTimerHandles;

    @Scope(RobopupuAppScope.class)
    @Provides(TimerManager.class)
    public TimerManagerImpl() {
        mTimerHandles = new HashMap<>();
    }

    @Override
    public TimerHandle createTimer(final Callback callback, final long delay) {
        final TimerHandle handle = new TimerHandle(this, callback, delay);
        mTimerHandles.put(handle.getId(), handle);
        handle.start();
        return handle;
    }

    protected void removeHandle(final TimerHandle handle) {
        mTimerHandles.remove(handle.getId());
    }

    @Override
    public void onUnplugged(final PluginBus bus) {
        super.onUnplugged(bus);

        for (final TimerHandle handle : mTimerHandles.values()) {
            handle.cancel();
        }
        mTimerHandles.clear();
    }
}

Note how the class TimerManagerImpl overrides and makes use of the framework method onPlugged(PluginBus) to cancel all pending timers  which are represented as instances of the class TimerHandle.

Calling Methods via a Plugin Bus

An example of using TimerManager via plugin bus can be found in the method  onViewStarted(View): of the class SplashPresenter.


package com.robopupu.feature.main.presenter;
import com.robopupu.component.TimerHandle;
import com.robopupu.component.TimerManager;
...
@Plugin
@Provides(SplashPresenter.class)
public class SplashPresenterImpl extends AbstractFeaturePresenter<SplashView>
        implements SplashPresenter {

    @Plug MainFeature mMainFeature;
    @Plug TimerManager mTimerManager;
    @Plug SplashView mView;

    @Override
    public SplashView getViewPlug() {
        return mView;
    }

    @Override
    public void onPlugged(final PluginBus bus) {
        super.onPlugged(bus);
        plug(SplashView.class);
    }

    @Override
    public void onViewStart(final View view) {
        super.onViewStart(view);

        mTimerManager.createTimer(new TimerManager.Callback() {
            @Override
            public void timeout(TimerHandle handle) {
                mMainFeature.openNavigationDrawer();
            }
        }, 3000L);
    }
}

Registering a Plugin Component to a Plugin Bus

Registering a plugin component to a plugin bus can be done by using one of the following static methods of PluginBus:

  1. plug(Object plugin):  Plugs the given Object as a plugin to PluginBus. The plugin can be an arbitrary object implementing one of more plugin interfaces, or a component that implements a PluginComponent interface  and one of more plugin interfaces.
  2. plug(Object plugin, boolean useHandler): Same as previous method, except the PlugInvoker added to PluginBus for the plugin will optionally use a HandlerInvoker object to transfer method invocation from non-main thread to main thread.
  3. plug(Class pluginClass): Creates an instance of the specified object using Robopupu.Dependency API and a currently active DependencyScope. Then invokes method plug(Object) to register the created instance as a plugin. If there is not any currently active DependencyScope, the application level DependencyScope is used.
  4. plug(Class pluginClass, DependencyScope scope):  Same as the previous method, but instead of using the currently active or application level DependencyScope, the given DependencyScope is used to resolve the specified dependency.

In the previous code sample, instance of SplashPresenterImpl uses method plug(Class) to create an instance of SplashView and to plug it into the plugin bus. Note that the presenter class has a @Plug annotated field mView which will be set when the created instance of the SplashView is plugged to plugin bus – that’s dependency injection.


@Override
public void onPlugged(final PluginBus bus) {
    super.onPlugged(bus);
    plug(SplashView.class);
}

 

Unregistering a Plugin Component

A registered plugin component can be easily unregistered using the static method unplug(Object) of PluginBus.

 Broadcasting Method Invocations

PluginBus can be used for one-to-many communication in similar way than event bus libraries (e.g. Greenrobots EventBus) where several subscribers can receive an event from a single publisher. To achieve this, a plug interface needs to be annotated with @PlugInterface(PlugMode.BROADCAST) like in this ExitObserver interface:


package com.robopupu.component;
import com.robopupu.api.plugin.PlugInterface;
import com.robopupu.api.plugin.PlugMode;

@PlugInterface(PlugMode.BROADCAST)
public interface ExitObserver {
    void onAppExit();
}

Now all the plugin components that implement the interface ExitObserver and are plugged into plugin bus will receive an invocation of method onAppExit() when the component AppManagerImpl invokes that same method in the method exitApplication() of the class AppManagerImpl:


package com.robopupu.component;
...
@Plugin
public class AppManagerImpl extends AbstractManager implements AppManager {
    private final RobopupuApplication mApplication;

    @Plug ExitObserver mExitObserver; // <--- INJECTED HERE
    @Plug PluginFeatureManager mFeatureManager;
 
    @Scope(RobopupuAppScope.class)
    @Provides(AppManager.class)
    public AppManagerImpl(final RobopupuApplication application) {
        mApplication = application;
    }
    ...
    @Override
    public void exitApplication() {
        final Activity activity = mFeatureManager.getForegroundActivity();
        final AlertDialog dialog = new AlertDialog.Builder(activity).create();
        dialog.setTitle(R.string.ft_main_dialog_title_exit_confirmation);
        dialog.setMessage(getString(R.string.ft_main_dialog_prompt_exit_app));
        dialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.ok),
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                    mExitObserver.onAppExit(); // <--- INVOCATION IN HERE
                    activity.finish();
                }
            });
        alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, 
                getString(R.string.cancel),
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    // Just dismiss the dialog
                    dialog.dismiss();
                }
            });
    alertDialog.show();
}

Declaring plugin interfaces to be used in broadcast mode has one important restriction: A such interface may not declare any method that returns a value. This is because calling a method that returns a value via plug for multiple objects is ambiguous: Which of the return values (provided by the method implementations in invoked components) should be returned as a return value for the method invoked for a plug?

When a plugin interface is declared to be used in reference mode this restriction does not apply, because a method is invoked for just a single component, and therefore the return value provided by the single invoked plugin component can be returned as a return value for method invoked for a plug.