Skip to main content
Version: v0.25.0 (Latest)

Events and UI Integration

The IDK provides an event system for building responsive user interfaces during mDoc presentations. This guide covers how to observe and handle events using the MdocEventHub.

Event Hub

The MdocEventHub is the central point for observing engagement and transfer events:

val engagementManager = session.graph.mdocEngagementManager
val eventHub: MdocEventHub = engagementManager.eventHub

// Combined stream of all events
eventHub.allEvents.collect { event ->
when (event) {
is MdocEngagementEvent -> handleEngagementEvent(event)
is MdocRetrievalEvent -> handleRetrievalEvent(event)
}
}

Event Types

Engagement Events

Engagement events track the state of connection establishment:

eventHub.engagementEvents.collect { event ->
val state: MdocEngagementState = event.state
val engagementId: Uuid = event.engagementId
val isActive: Boolean = event.isActive

when (state) {
is MdocEngagementState.Initializing -> {
showMessage("Setting up engagement...")
}
is MdocEngagementState.Connecting -> {
showMessage("Connecting to verifier...")
}
is MdocEngagementState.Connected -> {
showMessage("Connected to verifier")
}
is MdocEngagementState.Transferring -> {
showMessage("Transferring data...")
}
is MdocEngagementState.Completed -> {
showMessage("Transfer completed")
}
is MdocEngagementState.Error -> {
val error = state.error
showError("Error: ${error.message}")
}
}
}

MdocEngagementEvent Subtypes

The following table lists all MdocEngagementEvent subtypes and their meanings:

EventDescription
InitializingEngagement created
StartEngagement started
QrShowQR code should be displayed
QrHideQR code should be hidden
NfcEngagementNFC engagement detected
RestApiEngagementREST API engagement started
ConnectingTransport connection in progress
ConnectedTransport connected
DisconnectedTransport disconnected
CanceledUser or system canceled
ErrorError occurred
TransferTransfer phase started
DebugDebug information
DataData exchange (direction: IN/OUT)
for await event in eventHub.engagementEvents {
let state = event.state
let engagementId = event.engagementId
let isActive = event.isActive

switch state {
case .initializing:
showMessage(text: "Setting up engagement...")
case .connecting:
showMessage(text: "Connecting to verifier...")
case .connected:
showMessage(text: "Connected to verifier")
case .transferring:
showMessage(text: "Transferring data...")
case .completed:
showMessage(text: "Transfer completed")
case .error(let error):
showError(text: "Error: \(error.message)")
}
}

Retrieval Events

Retrieval events track the data transfer phase:

eventHub.retrievalEvents.collect { event ->
// Handle transfer/retrieval specific events
println("Retrieval event: $event")
}

MdocRetrievalEvent Subtypes

The following table lists all MdocRetrievalEvent subtypes and their meanings:

EventDescription
InitializingTransfer initialized
TransmissionTypeSelectedTransport method selected
SessionEstablishmentReceivedSession setup received from reader
DeviceRequestReceivedRequest from reader received
DeviceResponseSentResponse sent to reader
CompletedTransfer completed
ErrorTransfer error

Tracking State by Engagement

Get current state organized by engagement ID:

// Get current state by engagement ID
val statesByEngagement: StateFlow<Map<Uuid, MdocEngagementState>> =
eventHub.getCurrentStateByEngagement()

lifecycleScope.launch {
statesByEngagement.collect { states ->
for ((id, state) in states) {
println("Engagement $id: $state")
}
}
}

Recent Events for Debugging

Access recent events for debugging and diagnostics:

// Get recent events
val recentEvents: List<SequencedEvent> = eventHub.getRecentEvents(count = 10)

for (sequencedEvent in recentEvents) {
println("Event #${sequencedEvent.sequence}: ${sequencedEvent.event}")
}

When a verifier requests data, present a consent dialog:

class MdocConsentHandler(
private val engagementManager: MdocEngagementManager
) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

fun start() {
scope.launch {
engagementManager.eventHub.allEvents.collect { event ->
if (event is MdocEngagementEvent && event.state is MdocEngagementState.Connected) {
// Get the active engagement
val engagement = engagementManager.activeEngagement.value
if (engagement != null) {
handleConnectedEngagement(engagement)
}
}
}
}
}

private suspend fun handleConnectedEngagement(engagement: EngagementInstance) {
// Start transfer to receive request
val transferManager = engagement.start()

// Receive the device request
val deviceRequest = transferManager.receiveDeviceRequest()

// Show consent dialog
val approved = showConsentDialog(deviceRequest)

if (approved) {
// Create and send response
val deviceResponse = transferManager.createResponse(
deviceRequest = deviceRequest,
documentProvider = documentProvider
)
transferManager.sendDeviceResponse(deviceResponse)
} else {
// Send cancellation
val errorResponse = DeviceResponse.Builder()
.withStatus(DeviceResponseStatus(20u)) // User cancelled
.build()
transferManager.sendDeviceResponse(errorResponse)
}
}
}

Engagement Instance Events

Each engagement instance also provides its own event stream:

val engagement = engagementManager.activeEngagement.value

engagement?.events?.collect { event ->
val state = event.state
val isActive = engagement.isActive.value

when (state) {
is MdocEngagementState.Connected -> {
updateUI("Connected - ready to transfer")
}
is MdocEngagementState.Error -> {
updateUI("Error occurred")
}
}
}

Transfer Instance Tracking

Track active transfer instances:

// Track all active transfers
lifecycleScope.launch {
engagementManager.transferInstances.collect { transfers ->
for ((id, transfer) in transfers) {
println("Transfer $id active")
}
}
}

Simplified UI State

The event hub also provides a simplified, deterministic state model suitable for binding directly to UI frameworks:

  • eventHub.sessionEvents: SharedFlow<SessionEvent> emits discrete session lifecycle events.
  • eventHub.sessionState: StateFlow<SessionUiState> exposes a single SessionUiState value that always reflects the current phase of the session.

Because sessionState is a StateFlow, your UI layer can collect it and render a single view per state, without tracking multiple event streams or reconciling race conditions.

Error Handling

Handle errors gracefully in your UI:

eventHub.engagementEvents.collect { event ->
if (event.state is MdocEngagementState.Error) {
val error = (event.state as MdocEngagementState.Error).error

val message = when {
error.message.contains("timeout", ignoreCase = true) ->
"Connection timed out. Please try again."
error.message.contains("cancelled", ignoreCase = true) ->
"Operation was cancelled."
error.message.contains("bluetooth", ignoreCase = true) ->
"Bluetooth error. Please check your connection."
else -> error.message
}

showErrorDialog(message)

// Clean up
engagementManager.closeAll()
}
}

Filtering Events

The event hub provides helper methods for filtering events by state order, which is useful for narrowing down to specific lifecycle phases:

  • eventHub.eventsInStateRange(minOrder, maxOrder): returns events whose state order falls within the given range (inclusive). Useful for isolating a specific phase of the engagement or transfer lifecycle.
  • eventHub.finalStateEvents(): returns only terminal-state events (order >= 200). These represent completed, canceled, or errored engagements.
  • eventHub.activeTransferEvents(): returns events in the active transfer range (order 100-199). Useful for tracking in-progress data exchanges.
  • eventHub.onTransferCompletion: a convenience property that emits once when a transfer reaches a completion state. Ideal for triggering post-transfer logic such as navigation or cleanup.

Tips

  • Provide clear feedback. Users should understand whether they need to show a QR code, tap their device, or wait for a connection.
  • Make consent explicit. Show exactly what data will be shared and allow users to decline.
  • Handle all states. Success, error, and cancellation should all have appropriate UI responses.
  • Use the event hub. Subscribe to allEvents for full state tracking.
  • Test on real devices. Bluetooth and NFC behavior varies across devices.