Skip to content

Handlers

Handlers define how Transformers respond to incoming Signals and Effects. They contain the core business logic of your application and determine what happens when specific transmissions are received.

Overview

Handlers are defined within Transformers using the handlers DSL:

class MyTransformer : Transformer() {
    override val handlers: Handlers = handlers {
        onSignal<MySignal> { signal ->
            // Handle signal
        }

        onEffect<MyEffect> { effect ->
            // Handle effect
        }
    }
}

Signal Handlers

Signal handlers respond to incoming Signals from the UI or external sources.

Basic Signal Handler

override val handlers: Handlers = handlers {
    onSignal<UserLoginSignal> { signal ->
        // Access signal properties
        val credentials = signal.credentials

        // Perform business logic
        val result = authenticateUser(credentials)

        // Send data or publish effects
        if (result.isSuccess) {
            send(UserData(result.user))
            publish(NavigationEffect.GoToHome)
        } else {
            send(ErrorData("Login failed"))
        }
    }
}

Multiple Signal Handlers

override val handlers: Handlers = handlers {
    onSignal<IncrementSignal> {
        count++
        send(CounterData(count))
    }

    onSignal<DecrementSignal> {
        count--  
        send(CounterData(count))
    }

    onSignal<ResetSignal> {
        count = 0
        send(CounterData(count))
    }
}

Effect Handlers

Effect handlers respond to Effects, which can come from other Transformers or the same Transformer.

Basic Effect Handler

override val handlers: Handlers = handlers {
    onEffect<RefreshDataEffect> { effect ->
        // Handle refresh request
        val freshData = fetchDataFromServer()
        send(FreshDataLoaded(freshData))
    }

    onEffect<LoggingEffect> { effect ->
        // Handle logging
        logger.log(effect.level, effect.message)
    }
}

Chaining Effects

override val handlers: Handlers = handlers {
    onSignal<ProcessDataSignal> { signal ->
        // Start processing chain
        publish(ValidateDataEffect(signal.data))
    }

    onEffect<ValidateDataEffect> { effect ->
        val isValid = validateData(effect.data)
        if (isValid) {
            publish(SaveDataEffect(effect.data))
        } else {
            send(ValidationErrorData("Invalid data"))
        }
    }

    onEffect<SaveDataEffect> { effect ->
        saveData(effect.data)
        send(DataSavedSuccessfully(effect.data))
    }
}

Communication Scope

Within handlers, you have access to CommunicationScope which provides several operations:

Sending Data

Data is sent to the router's data stream for UI consumption:

onSignal<LoadUserSignal> { signal ->
    val user = loadUser(signal.userId)
    send(UserData(user)) // Available to UI via router.streamData()
}

Publishing Effects

Effects are published to other Transformers:

onSignal<UserActionSignal> { signal ->
    // Publish to any transformer listening for this effect
    publish(LoggingEffect("User performed: ${signal.action}"))

    // Publish to specific transformer
    publish(
        effect = NotificationEffect("Action completed"),
        identity = notificationTransformerIdentity
    )
}

Inter-Transformer Communication

Computing Values

onSignal<CalculateSignal> { signal ->
    // Get value from another transformer
    val currentData = compute(dataContract)

    // Compute with arguments
    val result = compute(calculationContract, signal.input)

    send(CalculationResult(result))
}

Executing Operations

onSignal<UserActionSignal> { signal ->
    // Fire-and-forget operation
    execute(logActionContract)

    // Execute with arguments
    execute(auditContract, AuditEntry(signal.action, System.currentTimeMillis()))

    send(ActionCompletedData())
}

Complete Examples

Counter Sample Handler

class Worker(val id: String) : Transformer() {

    override val handlers: Handlers = handlers {
        onSignal<CounterSignal.Lookup> {
            // Compute value from another transformer and send data
            send(CounterData("Transformer $id updated data to ${compute(lookUpAndReturn, id)}"))
        }
    }
}

Input Transformer from Components Sample

class InputTransformer(
    private val defaultDispatcher: CoroutineDispatcher
) : Transformer(dispatcher = defaultDispatcher) {

    private val holder = dataHolder(InputUiState(), holderContract)

    @OptIn(ExperimentalTransmissionApi::class)
    override val handlers: Handlers = handlers {
        onSignal<InputSignal.InputUpdate> { signal ->
            // Update local state
            holder.update { it.copy(writtenText = signal.value) }

            // Checkpoint-based communication (experimental)
            val color = pauseOn(colorCheckpoint)

            // Send effect to specific transformer
            send(
                effect = ColorPickerEffect.SelectedColorUpdate(color),
                identity = multiOutputTransformerIdentity
            )

            // Publish effect to all listeners
            publish(effect = InputEffect.InputUpdate(signal.value))
        }

        onEffect<ColorPickerEffect.BackgroundColorUpdate> { effect ->
            // Validate checkpoint
            validate(colorCheckpoint, effect.color)

            // Update state based on effect
            holder.update { it.copy(backgroundColor = effect.color) }
        }
    }
}

Complex Business Logic Handler

class OrderTransformer : Transformer() {
    private val orderHolder = dataHolder(OrderState(), orderContract)

    override val handlers: Handlers = handlers {
        onSignal<CreateOrderSignal> { signal ->
            // Start order creation process
            orderHolder.update { it.copy(isProcessing = true) }

            // Validate order
            publish(ValidateOrderEffect(signal.orderDetails))
        }

        onEffect<ValidateOrderEffect> { effect ->
            try {
                val validationResult = validateOrder(effect.orderDetails)

                if (validationResult.isValid) {
                    publish(ProcessPaymentEffect(effect.orderDetails))
                } else {
                    orderHolder.update { 
                        it.copy(
                            isProcessing = false,
                            error = validationResult.errorMessage
                        )
                    }
                }
            } catch (e: Exception) {
                orderHolder.update { 
                    it.copy(
                        isProcessing = false,
                        error = "Validation failed: ${e.message}"
                    )
                }
            }
        }

        onEffect<ProcessPaymentEffect> { effect ->
            try {
                val paymentResult = processPayment(effect.orderDetails.payment)

                if (paymentResult.isSuccess) {
                    publish(CreateOrderRecordEffect(effect.orderDetails, paymentResult.transactionId))
                } else {
                    orderHolder.update { 
                        it.copy(
                            isProcessing = false,
                            error = "Payment failed: ${paymentResult.error}"
                        )
                    }
                }
            } catch (e: Exception) {
                orderHolder.update { 
                    it.copy(
                        isProcessing = false,
                        error = "Payment processing error: ${e.message}"
                    )
                }
            }
        }

        onEffect<CreateOrderRecordEffect> { effect ->
            try {
                val order = createOrderRecord(effect.orderDetails, effect.transactionId)

                orderHolder.update { 
                    it.copy(
                        isProcessing = false,
                        completedOrder = order,
                        error = null
                    )
                }

                // Notify other systems
                publish(OrderCreatedEffect(order))
                execute(sendConfirmationEmailContract, order.customerEmail)

            } catch (e: Exception) {
                orderHolder.update { 
                    it.copy(
                        isProcessing = false,
                        error = "Order creation failed: ${e.message}"
                    )
                }
            }
        }

        onEffect<OrderCreatedEffect> { effect ->
            // Update analytics
            execute(trackOrderContract, AnalyticsEvent.OrderCreated(effect.order.id))
        }
    }
}

Error Handling in Handlers

Try-Catch in Handlers

override val handlers: Handlers = handlers {
    onSignal<RiskyOperationSignal> { signal ->
        try {
            val result = performRiskyOperation(signal.data)
            send(OperationSuccessData(result))
        } catch (e: NetworkException) {
            send(NetworkErrorData("Network error: ${e.message}"))
        } catch (e: ValidationException) {
            send(ValidationErrorData("Invalid input: ${e.message}"))
        } catch (e: Exception) {
            send(GenericErrorData("Unexpected error: ${e.message}"))
        }
    }
}

Using Result Types

override val handlers: Handlers = handlers {
    onSignal<LoadDataSignal> { signal ->
        when (val result = loadDataSafely(signal.id)) {
            is Result.Success -> {
                send(DataLoadedSuccessfully(result.data))
            }
            is Result.Failure -> {
                send(DataLoadingFailed(result.error))
                publish(LoggingEffect("Data loading failed: ${result.error}"))
            }
        }
    }
}

Advanced Handler Patterns

State Machine Pattern

class StateMachineTransformer : Transformer() {
    private val stateHolder = dataHolder(MachineState.Idle, stateContract)

    override val handlers: Handlers = handlers {
        onSignal<StartProcessSignal> { signal ->
            val currentState = stateHolder.getValue()

            when (currentState) {
                is MachineState.Idle -> {
                    stateHolder.update { MachineState.Processing(signal.data) }
                    publish(BeginProcessingEffect(signal.data))
                }
                is MachineState.Processing -> {
                    send(ErrorData("Process already running"))
                }
                is MachineState.Completed -> {
                    stateHolder.update { MachineState.Processing(signal.data) }
                    publish(BeginProcessingEffect(signal.data))
                }
            }
        }

        onEffect<ProcessCompletedEffect> { effect ->
            stateHolder.update { MachineState.Completed(effect.result) }
        }
    }
}

sealed class MachineState : Transmission.Data {
    object Idle : MachineState()
    data class Processing(val data: Any) : MachineState()
    data class Completed(val result: Any) : MachineState()
}

Command Pattern

class CommandTransformer : Transformer() {

    override val handlers: Handlers = handlers {
        onSignal<ExecuteCommandSignal> { signal ->
            when (val command = signal.command) {
                is Command.Save -> handleSaveCommand(command)
                is Command.Load -> handleLoadCommand(command)
                is Command.Delete -> handleDeleteCommand(command)
            }
        }
    }

    private suspend fun CommunicationScope.handleSaveCommand(command: Command.Save) {
        // Save logic
        saveData(command.data)
        send(CommandExecutedData("Save completed"))
    }

    private suspend fun CommunicationScope.handleLoadCommand(command: Command.Load) {
        // Load logic
        val data = loadData(command.id)
        send(DataLoadedData(data))
    }

    private suspend fun CommunicationScope.handleDeleteCommand(command: Command.Delete) {
        // Delete logic
        deleteData(command.id)
        send(CommandExecutedData("Delete completed"))
    }
}

sealed class Command {
    data class Save(val data: Any) : Command()
    data class Load(val id: String) : Command()
    data class Delete(val id: String) : Command()
}

Observer Pattern

class ObserverTransformer : Transformer() {
    private val observersHolder = dataHolder(
        initialValue = ObserverState(),
        contract = observerContract
    )

    override val handlers: Handlers = handlers {
        onSignal<RegisterObserverSignal> { signal ->
            observersHolder.update { state ->
                state.copy(observers = state.observers + signal.observer)
            }
        }

        onSignal<NotifyObserversSignal> { signal ->
            val observers = observersHolder.getValue().observers

            observers.forEach { observer ->
                publish(NotifyObserverEffect(observer, signal.event))
            }
        }

        onEffect<NotifyObserverEffect> { effect ->
            // Notify specific observer
            notifyObserver(effect.observer, effect.event)
        }
    }
}

Testing Handlers

Handlers are easily testable using the transmission-test module:

@Test
fun `should handle login signal correctly`() = transmissionTest {
    val transformer = AuthTransformer()

    // Send signal
    transformer.test {
        send(UserLoginSignal(validCredentials))

        // Verify data output
        expectData<UserData> { userData ->
            assertEquals("John Doe", userData.user.name)
        }

        // Verify effect output  
        expectEffect<NavigationEffect.GoToHome>()
    }
}

Best Practices

1. Keep Handlers Focused

// Good - single responsibility
onSignal<ValidateInputSignal> { signal ->
    val isValid = validateInput(signal.input)
    send(ValidationResult(isValid))
}

// Avoid - multiple responsibilities
onSignal<ProcessEverythingSignal> { signal ->
    validateInput(signal.input)
    saveToDatabase(signal.data)
    sendEmail(signal.email)
    updateUI(signal.uiData)
    logAction(signal.action)
}

2. Use Descriptive Signal/Effect Names

// Good - clear intent
onSignal<UserRequestsPasswordResetSignal> { /* ... */ }
onEffect<PasswordResetEmailSentEffect> { /* ... */ }

// Avoid - vague names
onSignal<UserSignal> { /* ... */ }
onEffect<SomeEffect> { /* ... */ }

3. Handle Errors Gracefully

onSignal<LoadDataSignal> { signal ->
    try {
        val data = loadData(signal.id)
        send(DataLoadedSuccessfully(data))
    } catch (e: Exception) {
        send(ErrorData("Failed to load data: ${e.message}"))
        execute(logErrorContract, e)
    }
}

4. Use Immutable Operations

// Good - immutable updates
onSignal<UpdateUserSignal> { signal ->
    userHolder.update { currentUser ->
        currentUser.copy(name = signal.newName)
    }
}

// Avoid - mutable operations
onSignal<UpdateUserSignal> { signal ->
    val user = userHolder.getValue()
    user.name = signal.newName // Mutation!
    userHolder.update { user }
}
// Good - clear flow
onSignal<CreateAccountSignal> { signal ->
    publish(ValidateAccountDataEffect(signal.accountData))
}

onEffect<ValidateAccountDataEffect> { effect ->
    if (isValid(effect.accountData)) {
        publish(CreateAccountRecordEffect(effect.accountData))
    } else {
        send(ValidationErrorData("Invalid account data"))
    }
}

onEffect<CreateAccountRecordEffect> { effect ->
    val account = createAccount(effect.accountData)
    send(AccountCreatedData(account))
    publish(SendWelcomeEmailEffect(account.email))
}