Here is an example that I wrote:
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright © 2021 MIT, All rights reserved.
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package edu.mit.appinventor.extensions.event_example;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.Component;
import com.google.appinventor.components.runtime.EventDispatcher;
import com.google.appinventor.components.runtime.Form;
import com.google.appinventor.components.runtime.OnClearListener;
import gnu.mapping.Environment;
import gnu.mapping.ProcedureN;
import gnu.mapping.SimpleSymbol;
import gnu.mapping.Symbol;
import gnu.mapping.Values;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The DynamicComponents extension demonstrates how to create a component at runtime and attach
* event listeners.
*
* @author Evan W. Patton (ewpatton@mit.edu)
*/
@DesignerComponent(version = 1, nonVisible = true, category = ComponentCategory.EXTENSION)
@SimpleObject(external = true)
public class DynamicComponents extends AndroidNonvisibleComponent implements OnClearListener {
/**
* A mapping of component names to the component instances they represent.
*/
private final Map<String, Component> components = new HashMap<>();
/**
* A mapping of qualified event name symbols to the implementation of the corresponding handler.
*/
private final Map<Symbol, EventHandler> eventHandlers = new HashMap<>();
/**
* The {@code EventHandler} class provides the basis for dispatching an event. In the normal
* operation of App Inventor, the Scheme code would generate a lambda function that would be
* invoked during the event handling.
*/
class EventHandler extends ProcedureN {
/**
* The component to which the event handler is bound.
*/
private final Component component;
/**
* The name of the event to which this handler corresponds.
*/
private final String name;
EventHandler(Component component, String name) {
this.component = component;
this.name = name;
}
@Override
public Object applyN(Object[] objects) {
EventFired(component, name, Arrays.asList(objects));
return Values.empty;
}
}
public DynamicComponents(Form form) {
super(form);
}
///region Methods
/**
* Creates a new component with the given name of the given type.
*
* @param type the type for the component, either an unqualified name in the App Inventor runtime
* or a fully qualified name for an extension.
* @param name the name of the component, as the user might call it.
* @return an instance of the component type, if the type exists and is a valid component type.
*/
@SimpleFunction
public Component CreateComponent(String type, String name) {
if (components.containsKey(name)) {
return components.get(name);
}
if (type.indexOf('.') == -1) {
type = "com.google.appinventor.components.runtime." + type;
}
Component result = makeComponent(type);
getFormEnvironment().put(new SimpleSymbol(name), result);
components.put(name, result);
return result;
}
/**
* Registers a handler for the given component name and the given event name. Returns true if
* the event was registered successfully, otherwise false.
*
* @param componentName the component name
* @param eventName the event name
* @return true if the event was registered successfully, otherwise false.
*/
@SimpleFunction
public boolean RegisterForEvent(String componentName, String eventName) {
Component component = components.get(componentName);
if (component == null) {
return false;
}
Symbol eventSymbol = SimpleSymbol.valueOf(componentName + "$" + eventName);
if (eventHandlers.containsKey(eventSymbol)) {
return false;
}
Environment environment = getFormEnvironment();
EventHandler h = new EventHandler(component, eventName);
eventHandlers.put(eventSymbol, h);
environment.put(eventSymbol, h);
EventDispatcher.registerEventForDelegation(form, componentName, eventName);
return true;
}
///endregion
///region Events
/**
* The EventFired event is raised when a registered event handler fires of the given event name
* for the given component previously registered with RegisterForEvent.
*
* @param component the component that raised the event
* @param event the name of the event raised
* @param arguments any additional arguments related to the event. this will always be a list,
* but the contents will vary from event to event.
*/
@SimpleEvent
public void EventFired(Component component, String event, List<Object> arguments) {
EventDispatcher.dispatchEvent(this, "EventFired", component, event, arguments);
}
///endregion
///region OnClearListener implementation
@Override
public void onClear() {
components.clear();
}
///endregion
///region Utilities
/**
* Utility function to create a new component of the given type.
*
* @param type the type of the component to create
* @return an instance of the component type
* @throws IllegalArgumentException if type does not implement Component
* @throws IllegalStateException if the type does not represent a Java Class object
*/
private Component makeComponent(String type) {
try {
Class<?> t = Class.forName(type);
if (!Component.class.isAssignableFrom(t)) {
throw new IllegalArgumentException(type + " does not correspond to a Component type");
}
for (Constructor<?> constructor : t.getConstructors()) {
Class<?>[] parameters = constructor.getParameterTypes();
if (parameters.length == 1 && parameters[0].isAssignableFrom(Form.class)) {
return (Component) constructor.newInstance(form);
}
}
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Unable to instantiate component of type " + type, e);
}
throw new IllegalStateException("Unable to instantiate component of type " + type);
}
/**
* Gets the form environment via reflection.
*
* @return the form environment
* @throws IllegalStateException if the form's environment cannot be retrieved via reflection
*/
private Environment getFormEnvironment() {
try {
Field f = form.getClass().getField("form$Mnenvironment");
return (Environment) f.get(form);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Unable to get form environment", e);
}
}
///endregion
}
And here is an example of how you could create a Clock and listen for its Timer event:
Note that I've only tested this in the companion. In theory it should work in compiled apps as well but I make no guarantees.