Middleware
Tuucho provides several middleware hooks that let you intercept and alter different parts of the system:
- Navigation (forward, back & finish)
- SendData operations
- Load Image operations
- View updates (contextual rendering)
Middleware execute in the exact order they are declared, thanks to bindOrdered. All middleware must be supplied to ModuleContextDomain.Middleware.
Navigation Middleware
Tuucho provides middleware for forward, backward and finish navigation.
object NavigationMiddleware {
/* in navigation context, NavigateToUrlUseCase.Input is the target url
data class Input(
val url: String
)
*/
fun interface ToUrl : MiddlewareProtocol<ToUrl.Context, Unit> {
data class Context(
val currentUrl: String?,
val input: NavigateToUrlUseCase.Input,
val onShadowerException: OnShadowerException?
)
}
fun interface Back : MiddlewareProtocol<Back.Context, Unit> {
data class Context(
val currentUrl: String,
val nextUrl: String?,
val onShadowerException: OnShadowerException?
)
}
fun interface Finish : MiddlewareProtocol<Unit>
}
Forward Navigation — NavigationMiddleware.ToUrl
You can use these hooks to alter the behaviour before or after navigation. Typical cases:
- Redirect to login when catching unauthenticated exceptions
- Log analytics before entering a protected or specific area
- Apply custom logic before transition
- Handle async loading issues via
onShadowerException - Simple logging
class BeforeNavigateToUrlMiddleware() : NavigationMiddleware.ToUrl {
override suspend fun process(
context: NavigationMiddleware.ToUrl.Context,
next: MiddlewareProtocol.Next<NavigationMiddleware.ToUrl.Context, Unit>?,
) {
// do something before
runCatching {
next.invoke(context.copy(
onShadowerException = {
exception, context, replay ->
// do something is case of async failure while loading contextual data
}
))
}.onFailure {
// do something is case of sync failure
}
// do something after
}
}
Register forward navigation middleware:
module(ModuleContextDomain.Middleware) {
factoryOf(::BeforeNavigateToUrlMiddleware) bindOrdered NavigationMiddleware.ToUrl::class
}
Make sure to use bindOrdered
Back Navigation — NavigationMiddleware.Back
Use NavigationMiddleware.Back to intercept backward navigation.
class BeforeNavigateBackMiddleware() : NavigationMiddleware.Back {
override suspend fun process(
context: NavigationMiddleware.Back.Context,
next: MiddlewareProtocol.Next<NavigationMiddleware.Back.Context, Unit>?,
) {
runCatching {
next.invoke(context.copy(
onShadowerException = {
exception, context, replay ->
// do something is case of async failure while loading contextual data
}
))
}.onFailure {
// do something is case of sync failure
}
// do something after
}
}
Register it:
module(ModuleContextDomain.Middleware) {
factoryOf(::BeforeNavigateBackMiddleware) bindOrdered NavigationMiddleware.Back::class
}
Make sure to use bindOrdered
Finish Navigation — NavigationMiddleware.Finish
Use NavigationMiddleware.Finish to get inform that Finish use case has been trigger either by yourself on action or automatically when the navigation stack get empty.
That's your responsibility to honor this demand by finishing Tuucho and/or the activity / uiController / application.
class NavigationFinishPublisher(
private val coroutineScopes: CoroutineScopesProtocol
) {
private val _events = Notifier.Emitter<Unit>(
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.SUSPEND
)
private val events get() = _events.createCollector
@OptIn(TuuchoInternalApi::class)
fun finish() {
coroutineScopes.default.asyncOnCompletionThrowing {
_events.emit(Unit)
}
}
@OptIn(TuuchoInternalApi::class)
fun onFinish(block: () -> Unit) {
coroutineScopes.main.asyncOnCompletionThrowing {
events.once { block() }
}
}
}
class NavigateFinishMiddleware(
private val navigationFinishPublisher: NavigationFinishPublisher
) : NavigationMiddleware.Finish {
override suspend fun process(
context: Unit,
next: MiddlewareProtocol.Next<Unit>?,
) {
next?.invoke(Unit)
navigationFinishPublisher.finish()
}
}
// Then inside where you start Tuucho (code available in sample application) :
// Android
AppScreen(
applicationModules = listOf(ApplicationModule.invoke(applicationContext)),
koinExtension = {
koin.get<NavigationFinishPublisher>().onFinish {
koin.close()
this@MainActivity.finish()
}
}
)
// iOS
let vc = KMPKitKt.uiView(koinExtension: { [weak self] koinApplication in
guard let self = self else { return }
let publisher = koinApplication.tuuchoKoinIos.get(clazz: NavigationFinishPublisher.self) as! NavigationFinishPublisher
publisher.onFinish(block: {
self.coordinator?.handleKoinClosed()
koinApplication.tuuchoKoinIos.close()
self.isKoinInitialized = false
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
})
})
Register it:
module(ModuleContextDomain.Middleware) {
singleOf(::NavigationFinishPublisher)
factoryOf(::NavigateFinishMiddleware) bindOrdered NavigationMiddleware.Finish::class
}
Make sure to use bindOrdered
SendData Middleware
SendDataMiddleware is used when sending data to the server and receiving a response.
For example:
- send form data and receive a server feedback
fun interface SendDataMiddleware : MiddlewareProtocol<Context, SendDataUseCase.Output> {
data class Context(
val input: SendDataUseCase.Input,
)
}
SendDataUseCase.Input:
data class Input(
val url: String,
val jsonObject: JsonObject,
)
url → the endpoint
jsonObject → the payload
SendDataUseCase.Output:
Returned output contains:
data class Output(
val jsonObject: JsonObject?,
)
Format details can be found in components-definition/form/
class SendDataMiddleware() : SendDataMiddleware {
override suspend fun process(
context: SendDataMiddleware.Context,
next: MiddlewareProtocol.Next<SendDataMiddleware.Context, SendDataUseCase.Output>?,
) = with(context.input) {
// do something before
ouput = next?.invoke(context)
// do something after
ouput
}
}
Register it:
module(ModuleContextDomain.Middleware) {
factoryOf(::SendDataMiddleware) bindOrdered SendDataMiddleware::class
}
UpdateView Middleware
Triggered whenever the view updates dynamically:
- contextual data received
- internal triggers (state changes)
- server-driven UI updates
Allows altering UI data before rendering.
class UpdateViewMiddleware() : UpdateViewMiddleware {
override suspend fun process(
context: UpdateViewMiddleware.Context,
next: MiddlewareProtocol.Next<UpdateViewMiddleware.Context, Unit>?,
) {
with(context.input) {
// do something before
next?.invoke(context)
// do something after
}
}
}
Input:
data class Input(
val route: NavigationRoute.Url,
val jsonObject: JsonObject,
)
jsonObject = Tuucho component / content / text, check the json documentation.
Register it:
module(ModuleGroupDomain.Middleware) {
factoryOf(::UpdateViewMiddleware) bindOrdered UpdateViewMiddleware::class
}
Middleware are executed in the order they are declared, through bindOrdered.