The Vapor Hook Framework - Global Custom Callbacks

The Vapor Hook Framework (VHF) is a powerful tool for Android app developers using Vapor. It allows you to create custom callbacks in your code, subscribe to them arbitrarily, and trigger them from anywhere - thanks to the VaporHookEngine (VHE).

Based primarily on the Observer Design Pattern, the Vapor Hook Framework provides a way for a set of interested subscribers (hookees) to register for asynchronous notifications from a given source object, whilst otherwise carrying on with their workload as normal.

Those experienced with jQuery might see some similarities with the jQuery method .trigger(...), however the Vapor Hook Framework is more customisable.

The open-ended VaporHooker ($hooker) and VaporHookee ($hookee) interfaces described below allow Android app developers to customise hooks specifically for each application.

Furthermore, developers can even prevent misuse and misfiring of a hook by assigning an admin auth token to it, and restrict which objects can subscribe to it by using a subscriber auth token.

The Vapor Hook Engine (VHE)

The VHE can be seen as a globally accessible Hook directory that enables you to create, manage, fire, and delete hooks from anywhere in your code.

Crucially this means you do not have to pass around a handle to a Hook in your app code.

The Hooker

The $hooker is an interface that clients can implement in order to have fine grained control over the behaviour of a hook.

You can set the Hooker for a hook using .hooker(...):

    $.hook("someHook").hooker(
        new $$hooker(){ // One-Size-Fits-All listener
    
            @Override
            public VaporBundle firstCall(VaporBundle args){

                return args.put("first", 1234).put("and", "hello");
            }
        }
    );
    

firstCall(VaporBundle)

This event is raised the first time the hook, to which it is attached, is fired.

This method is passed a VaporBundle, and also returns a VaporBundle . This gives the app developer the chance to modify the contents of the args VaporBundle sent to hookees (observers) the first time the hook is fired.

Return the modified VaporBundle from this method and Vapor will pass it to all hookees (observers) in place of the original args.

pre(VaporBundle args)

This event is raised prior to the hook, to which it is attached, being fired.

This method is passed a VaporBundle, and also returns a VaporBundle. This gives developers the chance to modify the contents of the args VaporBundle sent to hookees (observers) each time the hook is fired.

Return the modified VaporBundle from this method and Vapor will pass it to all hookees (observers) in place of the original args.

post(VaporBundle)

This event is raised after the hook, to which it is attached, is fired each time.

This gives the hooker the opportunity to examine the VaporBundle that was sent to hookees (observers), each time the hook is fired.

lastCall(VaporBundle)

This event is raised the last time the hook, to which it is attached, is fired.

This method is passed a VaporBundle, and also returns a VaporBundle. This gives the developer the chance to modify the contents of the args VaporBundle sent to hookees (observers) the last time the hook is fired.

Return the modified VaporBundle from this method and the Vapor Android framework will pass it to all hookees (observers) in place of the original args.

The Hookee

A 'hookee' is just another word for a hook subscriber, and the two terms are used interchangeably. The $hookee interface is very simple, consisting of only two methods: .call(String,VaporBundle) and .delete(String,VaporBundle).

call(String,VaporBundle)

This is the method that is called everytime the hook is fired. The String parameter is the firing hook's unique name, which helps you identify the notification source in the event that you have the same hookee registered to several hooks.

Each hookee is also passed a VaporBundle containing data from the hook that sent it. The content will almost certainly change over the course of the hook's lifetime, and can be used to send information about the current state of a part of the program to all hookees.

delete(String, VaporBundle)

This event is raised as the result of the VaporHookEngine (VHE) deleting or overwriting the hook in the system.

It is therefore not like a standard .call(String,VaporBundle), and hookees should therefore not rely on the contents of the VaporBundle being similar to that passed to each .call(String,VaporBundle) invocation.

Having said that, the hook admin can decide exactly what data is sent when by setting the Hooker.

Usage

There are two main ways in which a developer can provide a hookee to a hook in their Android app. If you are working with custom classes you may wish to have the class directly implement the $hookee interface as it is very simple.

This of course then allows you to provide an instance of the class as a $hookee without any wrapper:

       public class MyHookeeClass implements $hookee{
       
            @Override
            public void call(String hookName, VaporBundle args){
                ...
            }

            @Override
            public void delete(String hookName, VaporBundle args){
                ...
            }
       }
       ...

       MyHookeeClass myHookee = new MyHookeeClass();
       ...

       $.hook("someHook").hookIn(myHookee);
       

Alternatively, you can use the $$hookee<T> object as a wrapper.

$$hookee<T> expects a type parameter that matches the object's type that you intend to wrap. You should then pass this object to the constructor.

    public class MyActivity extends VaporActivity{
    
        public void someMethod(){
            ...
        }    

        @Override
        protected void create(VaporBundle savedInstanceState){
    
            $.hook("someHook").hookIn(new $$hookee<MyActivity>(this){
    
                @Override
                public void call(String hookName, VaporBundle args){
                    
                    $subject.someMethod(); // $subject is a handle to the wrapped object
                }
            }
        }
    
    }
    

Note how you can therefore freely refer to the object wrapped by the $$hookee using the $subject variable.

This handle is especially convenient if the object being wrapped otherwise goes out of scope.

In other situations in your Android app you might have a hookee not directly relevant to a single object. In such cases you can leave out the type parameter and use the empty $$hookee constructor:

       $.hook("someHook").hookIn(
            new $$hookee(){ // empty constructor with no type parameter
       
                @Override
                public void call(String hookName, VaporBundle args){
                    ...

                    // Here $subject will be null!
                }
            }
       );
       
If you fail to supply a type parameter to $$hookee, the inherent type of $subject will be erased to Object If you use the empty constructor for $$hookee, the $subject variable will remain null unless set in your code

Creating A Hook

Creating a Vapor Hook in the Vapor Android framework is incredibly easy, and flexible.

If you are happy to use the default settings (deleteable, with no admin or subscriber restrictions) then all you need supply is the unique hook name, and a new hook with that name will be created inside the VaporHookEngine:

						$.hook("myHookName");
					

The newly created fluent VaporHook object is returned, allowing you to use the new hook straightaway:

						$.hook("myHookName") // create new hook
							.hookIn(
								new $$hookee(...){...} // hook in some observer
							)
							.fire( // fire hook with some custom args

                                // chain methods just like in jQuery!
								$.Bundle().put("someArg",123).put("anotherArg", "Hello!")
							);
					

Hook Creation With VaporX

Moreover, the Vapor Hook Framework is a flavor of VaporX, allowing the app developer to create and manage multiple hooks simultaneously:

						$.hook("myHookName", "anotherHook", "aThirdHook") // create new hooks using VaporX
							.hookIn(
								new $$hookee(...){...} // hook in some observer to ALL hooks
							)
							.fire( // fire ALL hooks with the same custom args
								$.Bundle().put("someArg",123).put("anotherArg", "Hello!")
							);
					

Using VaporX we apply the same operation on all constituent hooks.

In the example above we create 3 hooks, subscribe to all 3 simultaneously, and fire all 3 at once!

Naming Conventions

It is recommended you follow a naming convention for all hooks you create during development. The Vapor Android framework adopts a camelCase convention, starting with a lowercase first letter.

For example, VaporActivity.CONFIG_CHANGED is "configChanged", and VaporActivity.ORIENTATION_LANDSCAPE is "orientationLandscape"

See more examples of the core Vapor hooks in the System Hooks section

Naming Collisions

If a hook with a given name already exists, the VHE will attempt to overwrite the hook currently mapped to the name.

To check whether a given hook name already exists in the VHE you can use the boolean function $.hooks().exists("hookName"). This method returns true only if a hook with the given name already exists in the VaporHookEngine

You can also use $.hooks().uniqueName("hookName") to obtain a guaranteed unique name for your hook in any case. This method will return a String that you can use as a unique hook name.

If the original hook name did not clash with any existing hook, the returned String will be the same as the original hook name.

If the existing hook is not .deleteable(), or is .adminRestricted() and you do not supply the auth token, any attempt to overwrite the hook will fail.

MultiHooks (VaporX)

As with VaporViews, the VaporX framework allows you to manage more than one hook simultaneously - similar to how jQuery selectors work.

A VaporXHook allows you to perform the same operation as you would on a single hook, automatically invoking the operation on all members in the underlying group.

As an example, consider you are writing a game and want to inform other parts of your code when the user gets Game Over and has obtained a new score.

We will be sending both hooks the same args VaporBundle of data, so to keep things compact we get a VaporXHook to do this for us:

        ...

		if(userHealth <= 0){
			$.hook(GAME_OVER_HOOK,NEW_SCORE_HOOK).fire($.Bundle().put("score", userScore));
		}
	

Notice how you still use the $.hook(...) method. When you supply more than one selector to this method it returns a VaporXHook automatically, as opposed to a singular VaporHook.

It follows that any methods you invoke on a VaporXHook will return an aggregate ArrayList of results gathered from invoking the same method on all members of the underlying VaporHook group.

Selectors

A VaporXHook can be instantiated using any mixed combination of the following acceptable types:

  • String - the unique name of a VaporHook
  • VaporHook - an existing VaporHook instance
VaporHooks identified by a String selector will be created using default settings if they do not already exist in the VaporHookEngine

Obtaining A Hook Handle

To obtain a handle to hooks that have already been created you also use the $.hook("hookName") method.

This method only creates a new hook if one does not already exist with the given name. Otherwise, it just returns a handle to the hook mapped to the given name:

			$.hook("myHook"); // creates and returns a new hook
			...
			
			// returns the existing hook mapped to "myHook"
			$.hook("myHook")... 
		   

This also applies to using VaporX to obtain a handle to a group of hooks:

			$.hook("myHook","someOtherHook","anotherHook"); // creates and returns the new hooks
			...
			
			// just returns the existing hooks mapped to "myHook","someOtherHook" and "anotherHook"
			$.hook("myHook","someOtherHook","anotherHook")... 
		   

Subscring To A Hook

In Vapor terms subscribing to a hook is known as hooking in, and likewise, unsubscring is known as hooking out.

hookIn(...)

The .hookIn(...) method has several overloads designed to accept one or more $hookee's in different forms. The acceptable forms include ArrayList<$hookee<?>> and varargs lists of $hookee<?>, allowing for flexibility during app development:

			$.hook("myHook").hookIn( // hook in to "myHook"
			
				new $$hookee(...){...} // a subscriber
				,new $$hookee(...){...} // another subscriber
				,new $$hookee(...){...} // a third subscriber
			);
		

If your hook is .subscriberRestricted() you will additionally need to pass the subscriber auth token as the first argument to .hookIn(...):

			$.hook("myHook").hookIn( // hook in to "myHook"
				
				// supply the subscriber auth as first arg if its required
				"mySubscriberAuthToken"
				
				// then give your list of subscribers as before
				,new $$hookee(...){...} // a subscriber
				,new $$hookee(...){...} // another subscriber
				,new $$hookee(...){...} // a third subscriber
			);
		

hookOut(...)

The hook out process is very much the same as the above hook in code:

			$.hook("myHook").hookOut( // hook out from "myHook"
			
				someHookeeInstance // a subscriber
				,anotherHookeeInstance // another subscriber
				,aThirdHookeeInstance // a third subscriber
			);
		

Again, if the hook is .subscriberRestricted() you will need to provide the subscriber auth token as the first argument to .hookOut(...):

			$.hook("myHook").hookIn( // hook out from "myHook"
				
				// supply the subscriber auth as first arg if its required
				"mySubscriberAuthToken"
				
				// then give your list of subscribers as before
				,someHookeeInstance // a subscriber
				,anotherHookeeInstance // another subscriber
				,aThirdHookeeInstance // a third subscriber
			);
		

Firing A Hook

Firing a hook in the Vapor Hook Framework involves using the .fire(...) method. The most simple overload of this method fires the hook with an empty VaporBundle of data sent to observers:

						// hook is fired with empty data bundle passed to observers
						$.hook("someHook").fire(); 
					

If the hook in question is .adminRestricted() the first argument you supply to .fire(...) needs to be a valid auth token:

						// hook is fired using admin auth, without any args passed to observers
						$.hook("someHook").fire(
							// supply the admin auth as first arg if its required
							"adminAuthToken"
						); 
					

Sending Data To Hookees

To pass data to your hook's observers you use a VaporBundle, in which you put your custom data:

						// hook is fired without any args passed to observers
						$.hook("someHook").fire(
							$.Bundle() // create a bundle of custom data
									.put("someData", 123)
									.put("someOtherData", "Hello!")
						); 
					

And likewise, with admin auth:

						// hook is fired using admin auth, without any args passed to observers
						$.hook("someHook").fire(
							// supply the admin auth as first arg if its required
							"adminAuthToken"
							
							// create a bundle of custom data
							,$.Bundle() 
										.put("someData", 123)
										.put("someOtherData", "Hello!")
						); 
					

Null Pointer Protection

Whilst app developers should always make sure observers (hookees) are hooked out from a hook when appropriate during the lifetime of their Android app, if you forget to do so the Vapor Hook Framework will do its best to detect this.

When a hook is fired the Vapor Android framework checks whether each $$hookee is null. If so, it removes the observer automatically and notifies you via the console, thus preventing a NullPointerException being thrown.

The Vapor Hook Framework will not check if hookeeInstance.subject() == null, as this will be null inherently if the parameterless constructor was used to create the hookee

Overwriting A Hook

In the simplest scenario, to overwrite a hook you use the $.hook(String, VaporBundle) method, where the String argument is the name of an already established hook, and the VaporBundle contains the new settings:

					$.hook("someHook");
					...
					
					// overwrite the hook by supplying new settings
					$.hook("someHook", 
							$.Bundle() // new hook settings go here
					)
				

However, this only works if the hook being overwritten is .deleteable(), and not .adminRestricted().

To make a hook deleteable use .deleteable(...):

					// if the hook does not require admin auth
					$.hook("myHook").deleteable(true);
					
					// if the hook does require admin auth
					$.hook("myHook").deleteable(
						"adminAuthToken" // supply valid admin auth
						,true
					);
				

If the hook is .adminRestricted() you can only replace it using a settings VaporBundle that contains the same admin auth token:

					$.hook("existingHookName", 
					
						$.Bundle() // new settings
						...
						
						// make sure we put same admin auth token as original hook
						.put("adminAuth", "theSameAdminAuthToken")
					);
				

This will overwrite the original hook with a new VaporHook created from the supplied settings.

Deleting A Hook

In the simplest case you can delete a hook in your Android app by simply invoking .delete() on it;

However, if the hook is .adminRestricted() you will need to supply the admin auth token as the first argument to .delete(...):

                    $.hook("myHook").delete("validAdminAuthToken");
                

If the hook is not .deleteable() then you cannot delete it until you call .deleteable(true), again using admin auth if required:

                     VaporHook theHook = $.hook("myHook");
        
                     if(!theHook.deleteable()){

                        // no admin auth required
                        theHook.deleteable(true);
                     }

                     // no admin auth required
                     theHook.delete();
                     

In the above example we delete a hook that is not .adminRestricted(). In the following example we delete a hook that requires admin auth:

                     VaporHook theHook = $.hook("myHook");
        
                     if(!theHook.deleteable()){

                        // using admin auth as required
                        theHook.deleteable("validAdminAuthToken", true);
                     }

                     // using admin auth as required
                     theHook.delete("validAdminAuthToken");
                     

Admin Auth

Some hook operations such as .fire(...) and .delete() can be password protected by the creator of the hook. This is to prevent misuse of the hook by unauthorized parts of your Android app.

By default no password is required for general hooks, but one should be set for hooks that should only be fired and controlled by specific parts of your application.

To admin restrict your hook, provide an admin auth token in the settings used to create the hook:

    $.hook("myNewHook", 
        $.Bundle()
            ...

            // admin restrict your hook with an admin auth token
            .put("adminAuth", "myAdminAuthToken")
    );
    

Subscriber Auth

A developer may want a hook to only be observable by authorized objects in their Android app. To achieve this set a subscriber auth token when creating the hook:

    $.hook("myNewHook", 
        $.Bundle()
            ...

            // restrict subscribers to your hook with a subscriber auth token
            .put("subscriberAuth", "mySubscriberAuthToken")
    );
    

This will mean that potential observers will be required to supply the subscriber auth token to the framework when requesting to .hookIn() to your hook. By default no password is required for general hooks, but one should be set for hooks you intend to restrict to exclusive types.

Tips for Custom Auth Policies

You might find that you want admin auth set on some operations, but might want to make other operations freely accessible to any parts of your Android app. What's more, you want to do this without having to expose your admin auth token.

This is equally straightforward to accomplish; in your class that has admin rights to the hook, just define helper methods that silently pass the admin auth token to the VHE/hook handle.

As a result the caller is able to access the previously restricted hook operation without needing to supply an admin auth token, or be given access to the admin auth token directly. The example below demonstrates this:

      public class CustomAuthPolicy{
        
        private VaporHook hook;
        private String hookName, adminAuth = "someSecretAdminAuth";  

        public CustomAuthPolicy(){
            // make sure this hook name is unique
            this.hookName = $.hooks().uniqueName("myHook");
            
            this.hook = $.hook(hookName, 
                            // admin restrict our hook
                            $.Bundle().put("adminAuth","someSecretAdminAuth")
                        );
        }

        ...

        // we let clients fire the hook WITHOUT the admin auth
        public void fire(){

           // we silently supply the admin auth implicitly
           this.hook.fire(adminAuth);       
        }
      
        ...
      
      }
  

Here our custom policy lets clients fire the admin restricted hook without needing to know the admin auth.

You could also use this technique to set additional custom passwords. To do this you should still keep your admin auth private, but expose methods for hook operations that take a custom password as an argument. You can then decide for each method what the password is you accept:

      public class CustomAuthPolicy{
        
        private VaporHook hook;
        private String hookName, adminAuth = "someSecretAdminAuth",
        fireAuth = "someAdditionalAuthToken", deleteAuth = "anotherAdditionalAuthToken";  

        public CustomAuthPolicy(){
            // make sure this hook name is unique
            this.hookName = $.hooks().uniqueName("myHook");
            
            this.hook = $.hook(hookName, 
                            // admin restrict our hook
                            $.Bundle().put("adminAuth","someSecretAdminAuth")
                        );
        }

        ...

        // we require an additional custom fire auth token to fire the hook
        public void fire(String fireAuth){

            if(fireAuth.equals(this.fireAuth)){
               // we silently supply the actual admin auth implicitly
               this.hook.fire(adminAuth); 
            }      
        }
      
       // we require an additional custom fire auth token to delete the hook
        public void delete(String deleteAuth){

            if(deleteAuth.equals(this.deleteAuth)){
               // we silently supply the actual admin auth implicitly
               this.hook.delete(adminAuth);  
            }     
        }

        ...
      
      }

  

In the above example we create individual auth tokens for particular operations.


These are just a few examples of how you are totally free to customise hooks in the Android apps you develop with the powerful Vapor Android framework.