Communication with the MasterController (Android/Kotlin and Flutter)
We communicate with the Master Controller by sending and receiving events. We do this through the SynchronousEventHandler.
Android/Kotlin Implementation
We mainly use three methods of the SynchronousEventHandler to communicate with the Master Controller:
-
postEvent(event: EventFrameworkEvent)
: This method sends an event to the Master Controller. We use it when we expect a result event after triggering an event on the Master Controller side. By implementing thethen{}
block onpostEvent()
, we can retrieve and handle the result event. ThepostEvent()
method returns a Future object, and itsget()
method returns an EventFrameworkEvent, which is the superclass of all events. -
forwardEvent(event: EventFrameworkEvent)
: Similar topostEvent()
, this method sends events to the Master Controller. However, it operates as a "Fire and Forget" mechanism, meaning it does not expect a result event for the triggered event. We use this method when no result event is expected; however, there is nothing that speaks against usingpostEvent()
in such cases. Here you can find the events table. -
executeEvent(eventFrameworkEvent: EventFrameworkEvent?)
: This method receives events from the Master Controller. You should override it to make your app react to events/result events coming from the MC.
We suggest that you override executeEvent in some kind of global observer in your app to ensure you can always react to a RestartEvent sent by the MC.
override fun executeEvent(event: EventFrameworkEvent?) {
when (event) {
is RestartResultEvent -> {
when (event.sdkState) {
SdkState.ACTIVATION_REQUIRED -> {
// go to activation screen...
}
SdkState.LOGIN_REQUIRED -> {
// go to login screen...
}
else -> {
// unexpected...
}
}
}
// ...
}
}
Non-Blocking Asynchronous Handling with then{}
Block
The recommended approach for handling postEvent results is using the then{}
block, which allows you to handle the result event asynchronously without blocking the calling thread.
Basic then{}
Usage
synchronousEventHandler.postEvent(restartEvent)?.then { resultEvent ->
when (resultEvent) {
is RestartResultEvent -> {
// Handle restart result
when (resultEvent.sdkState) {
SdkState.ACTIVATION_REQUIRED -> {
// Navigate to activation screen
}
SdkState.LOGIN_REQUIRED -> {
// Navigate to login screen
}
SdkState.LOGGED_IN -> {
// Navigate to main screen
}
}
}
// Handle other result types...
}
}
// This code executes immediately without waiting for the result
println("Event sent, continuing with other operations...")
Advantage: Code execution continues immediately after sending the event, without waiting for the result. The
then{}
block executes asynchronously when the result is received.
Key Benefits of then{}
Block
- Non-blocking: UI remains responsive while waiting for results
- Asynchronous: Result handling happens when the response arrives
- Thread-safe: Callbacks execute on appropriate threads
- Clean code: Avoids complex threading and callback management
- Error handling: Easy to implement try-catch blocks within the callback
Best Practice: Use
then{}
block for most postEvent operations to maintain responsive UI and clean asynchronous code flow. Reserve blocking methods (get()
,waitResult()
,waitResultFor()
) for special cases where synchronous behavior is required.
MC-Future Methods for Synchronous Handling
The Future object returned by postEvent()
provides several methods for blocking operations:
get()
- Block Until Future Resolved
val future = synchronousEventHandler.postEvent(event)
val resultEvent = future.get() // Blocks until future is resolved
// Handle the result event
⚠️ IMPORTANT: The
get()
method blocks the calling thread until the future is resolved. Use carefully to avoid blocking the UI thread.
waitResult()
- Block Until Future Resolved (No Return)
val future = synchronousEventHandler.postEvent(event)
future.waitResult() // Blocks until future is resolved (no return value)
// Future is now resolved
waitResultFor(milliseconds)
- Block with Timeout
val future = synchronousEventHandler.postEvent(event)
val status = future.waitResultFor(5000) // Wait max 5 seconds
when (status) {
Status.READY -> {
// Future resolved within timeout
val resultEvent = future.get()
// Handle result
}
Status.TIMEOUT -> {
// Future did not resolve within timeout period
// Handle timeout case
}
}
Usage Examples with Blocking Operations
// Example 1: Direct blocking call
fun performSynchronousLogin(userIdentifier: UserIdentifier): LoginResultEvent {
val loginEvent = LoginEvent(userIdentifier)
val resultEvent = synchronousEventHandler.postEvent(loginEvent)?.get() // Blocks here
return resultEvent as LoginResultEvent
}
// Example 2: Blocking with timeout
fun performLoginWithTimeout(userIdentifier: UserIdentifier): LoginResultEvent? {
val loginEvent = LoginEvent(userIdentifier)
val future = synchronousEventHandler.postEvent(loginEvent)
val status = future?.waitResultFor(10000) // Wait max 10 seconds
return if (status == Status.READY) {
future.get() as LoginResultEvent
} else {
null // Timeout occurred
}
}
⚠️ Threading Warning: These blocking methods will block the calling thread. Avoid using them on the main/UI thread, as this will freeze the user interface. If you need to use these methods, ensure they are only called in background threads or coroutines.
Flutter/Dart Implementation
For Flutter, we use a helper class called McWrapperHandler in our demo app. The McWrapperHandler class serves as a wrapper around the McWrapperApi and provides methods for communicating with the Master Controller.
Key Features of McWrapperHandler:
- Singleton Pattern: Ensures a single instance throughout the app lifecycle
- Event Sending: Uses the
send()
method to communicate with the Master Controller - Event Receiving: Implements
McWrapperApiEventReceiver
to handle incoming events - Observer Pattern: Allows multiple observers to listen for Master Controller events
- Lifecycle Management: Provides start, stop, suspend, and resume functionality
class McWrapperHandler extends McWrapperApiEventReceiver {
late McWrapperApi _mcWrapperApi;
static final McWrapperHandler _instance = McWrapperHandler._internal();
List<McWrapperApiEventReceiver> receivers = [];
McWrapperApi get api => _mcWrapperApi;
factory McWrapperHandler() {
return _instance;
}
}
Flutter/Dart Usage Examples
Getting Instance and Initialization
// Get the singleton instance
final mcHandler = McWrapperHandler();
// Or get from service locator (see below for further details)
// final mcHandler = locator<McWrapperHandler>();
// Initialize the wrapper
await mcHandler.initializerMethod();
Sending Events to the Master Controller
Future<void> sendEventToMC() async {
try {
// Create your event
final startEvent = StartEventT();
// Send event and handle response
final response = await mcHandler.send(startEvent);
response.fold(
(error) {
print("❌ Error sending event: $error");
},
(result) {
print("✅ Event sent successfully: $result");
// Handle the result event
},
);
} catch (e) {
print("❌ Exception: $e");
}
}
Adding Event Observers
class MyEventReceiver extends McWrapperApiEventReceiver {
@override
void onReceive(Object event) {
print("📥 Received event: ${event.runtimeType}");
// Handle specific event types
if (event is TriggerBannerEventT) {
handleTransactionEvent(event);
} else if (event is StartResultEventT) {
handleStartResult(event);
}
}
void handleTransactionEvent(TriggerBannerEventT event) {
// Handle transaction events
}
void handleStartResult(StartResultEventT event) {
// Handle start result events
}
}
// Add the observer
void setupEventHandling() {
final mcHandler = locator<McWrapperHandler>();
final eventReceiver = MyEventReceiver();
mcHandler.addObserver(eventReceiver);
}
Removing Event Observers
void cleanupEventHandling() {
final mcHandler = locator<McWrapperHandler>();
final eventReceiver = myEventReceiverInstance;
mcHandler.removeObserver(eventReceiver);
}
Lifecycle Management
class MyApp extends StatefulWidget with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final mcHandler = locator<McWrapperHandler>();
switch (state) {
case AppLifecycleState.resumed:
mcHandler.resumeWrapper();
break;
case AppLifecycleState.paused:
mcHandler.suspendWrapper();
break;
case AppLifecycleState.detached:
mcHandler.stopWrapper();
break;
default:
break;
}
}
}
Flutter Service Locator Integration
In Flutter, we use a service locator (GetIt) to manage dependencies and provide global access to the McWrapperHandler instance. This approach offers several benefits:
- Singleton Management: Ensures only one instance of McWrapperHandler exists throughout the app
- Global Access: Any part of the app can access the handler without passing it through constructors
- Dependency Injection: Simplifies testing by allowing easy substitution of mock implementations
- Lifecycle Management: Centralized control over when services are created and destroyed
- Decoupling: Reduces dependencies between classes by providing a central registry
GetIt locator = GetIt.instance;
Future<void> setupDependencies() async {
// Initialize McWrapperHandler
final mcHandler = McWrapperHandler();
await mcHandler.initializerMethod();
// Register in service locator as singleton
locator.registerSingleton<McWrapperHandler>(mcHandler);
// Add observers for transaction handling
final transactionReceiver = TransactionEventReceiver();
mcHandler.addObserver(transactionReceiver);
locator.registerLazySingleton(() => transactionReceiver);
}
// Access from anywhere in the app
void useFromAnywhereInApp() {
final mcHandler = locator<McWrapperHandler>();
// Use the handler...
}
Important Notes
NOTE:
- Always remove observers when they are no longer needed to prevent memory leaks
- Handle app lifecycle events to properly suspend/resume the wrapper