Skip to main content
Docs: template-java

Stubs

The stubs feature generates:

  • Fully instantiable implementation classes for each API interface
  • A starting point for your business logic
  • Property storage with change detection and listener notification
  • Async operation support via CompletableFuture and ExecutorService

The stub implementations extend the abstract base classes generated by the api feature and provide complete, working implementations that you can customize.

note

For cross-process communication on Android, see the android feature. The Android service uses stubs as its default backend.

caution

The stubs module is an Android library (com.android.library plugin) and requires the Android SDK to build. It cannot be used in pure Java (non-Android) projects.

File overview

With our example API definition:

Hello World API (click to expand)
schema: apigear.module/1.0
name: io.world
version: "1.0.0"

interfaces:
- name: Hello
properties:
- { name: last, type: Message }
operations:
- name: say
params:
- { name: msg, type: Message }
- { name: when, type: When }
return:
type: int
signals:
- name: justSaid
params:
- { name: msg, type: Message }
enums:
- name: When
members:
- { name: Now, value: 0 }
- { name: Soon, value: 1 }
- { name: Never, value: 2 }
structs:
- name: Message
fields:
- { name: content, type: string }

The following file structure is generated in the ioWorld_impl module:

📂ioWorld/ioWorld_impl
┣ 📜build.gradle
┗ 📂src/main/java/ioWorld/ioWorld_impl
┗ 📜HelloService.java

Implementation

HelloService extends AbstractHello and provides a complete implementation of the IHello interface. It serves as a local backend that can be used directly or as the backend for the android service adapter.

Properties

Properties are stored as private fields with change detection in the setter:

public class HelloService extends AbstractHello {
private Message m_last = new Message();

@Override
public void setLast(Message last) {
if ((m_last != null && !m_last.equals(last))
|| (m_last == null && last != null)) {
m_last = last;
onLastChanged(m_last);
}
}

private void onLastChanged(Message newValue) {
fireLastChanged(newValue);
}

@Override
public Message getLast() {
return m_last;
}
}

The setter:

  1. Checks if the value actually changed using equals()
  2. Updates the stored value
  3. Calls onLastChanged(), which notifies all listeners via the inherited fireLastChanged()

Operations

Synchronous operations are generated with stub implementations that return default values:

@Override
public int say(Message msg, When when) {
return 0;
}

Fill in your business logic by modifying the method body.

Async Operations

Async operations are implemented using CompletableFuture backed by a single-thread ExecutorService:

private static final ExecutorService executor = Executors.newSingleThreadExecutor();

@Override
public CompletableFuture<Integer> sayAsync(Message msg, When when) {
return CompletableFuture.supplyAsync(
() -> say(msg, when),
executor);
}

The async variant wraps the synchronous implementation, so you only need to implement the sync method. Override sayAsync if you need custom async behavior.

Signals

To emit a signal from your implementation, use the inherited fire methods:

Message msg = new Message("Something happened");
fireJustSaid(msg);

This notifies all registered event listeners. When the service runs behind the android adapter, the signal is automatically forwarded to all IPC clients.

Ready State

The stub service marks itself as ready immediately on construction:

public HelloService() {
fire_readyStatusChanged(true);
}

@Override
public boolean _isReady() {
return isServiceReady;
}

Usage

Service side

The stubs serve as the default backend for the android service. Use HelloServiceStarter to manage the service lifecycle — it creates the backend, starts the Android service, and returns the backend instance:

// Start the service and get the backend instance
HelloService backend = (HelloService) HelloServiceStarter.start(getApplicationContext());
backend.addEventListener(this);

// Interact with the backend directly (changes are forwarded to IPC clients)
backend.setLast(new Message("Hello from service"));

Stop the service when your activity or application is done:

// In onDestroy() or equivalent
backend.removeEventListener(this);
HelloServiceStarter.stop(getApplicationContext());

The starter also exposes a ServiceLifecycleListener interface to receive connection and binding-death events.

Client side

From another app, bind to the service using HelloClient (from the android feature). The client implements the same IHello interface, so your application code works identically regardless of whether the backend is local or remote:

HelloClient client = new HelloClient(getApplicationContext(), "main");
client.bindToService("com.example.serviceapp");

// Wait for the connection to be established
client.addEventListener(new IHelloEventListener() {
@Override
public void on_readyStatusChanged(boolean isReady) {
if (isReady) {
// Now safe to use the client
client.setLast(new Message("Hello from client"));
}
}

@Override
public void onLastChanged(Message newValue) {
// Received property change from service
}

@Override
public void onJustSaid(Message msg) {
// Received signal from service
}
});

// Later
client.unbindFromService();

See the android feature documentation for the full service/client architecture.

Thread Safety

The listener collection (inherited from AbstractHello) uses ConcurrentHashMap.newKeySet(), which is safe for concurrent add/remove/iterate. However, the ExecutorService runs async operations on a separate thread. If your operation implementation accesses shared state, ensure proper synchronization.

Gradle Module

The ioWorld_impl module is an Android library (com.android.library plugin) that depends on the ioWorld_api module. It requires the Android SDK for logging (android.util.Log).