Is it a good design to define all events in the entrance UI? - kotlin

The Code A is from a official sample project.
1: The author define all events such as onAddItem, onRemoveItem ... in the entrance UI fun TodoScreen, is it a good design?
2: You know the fun TodoScree will become huge when the functionality of the APP is increased, how can I improve the app architecture ?
Code A
#Composable
fun TodoScreen(
items: List<TodoItem>,
currentlyEditing: TodoItem?,
onAddItem: (TodoItem) -> Unit,
onRemoveItem: (TodoItem) -> Unit,
onStartEdit: (TodoItem) -> Unit,
onEditItemChange: (TodoItem) -> Unit,
onEditDone: () -> Unit
) {
...
}
class TodoActivity : AppCompatActivity() {
val todoViewModel by viewModels<TodoViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StateCodelabTheme {
Surface {
TodoActivityScreen(todoViewModel)
}
}
}
}
}
#Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
TodoScreen(
items = todoViewModel.todoItems,
currentlyEditing = todoViewModel.currentEditItem,
onAddItem = todoViewModel::addItem,
onRemoveItem = todoViewModel::removeItem,
onStartEdit = todoViewModel::onEditItemSelected,
onEditItemChange = todoViewModel::onEditItemChange,
onEditDone = todoViewModel::onEditDone
)
}

This design pattern is called state hoisting, and is explained in the docs:
https://developer.android.com/jetpack/compose/state#state-hoisting
State hoisting in Compose is a pattern of moving state to a
composable's caller to make a composable stateless. State that is
hoisted this way has some important properties:
Single source of truth: By moving state instead of duplicating it, we're ensuring there's only one source of truth. This helps avoid
bugs. Encapsulated: Only stateful composables will be able to modify
their state. It's completely internal.
Shareable: Hoisted state can be shared with multiple composables. Say we wanted to name in a different composable, hoisting would allow
us to do that.
Interceptable: callers to the stateless composables can decide to ignore or modify events before changing the state.
Decoupled: the state for the stateless ExpandingCard may be stored anywhere. For example, it's now possible to move name into a
ViewModel.
About the issue with "my screen constructor is gonna be huge".
For lower level components i like to follow the pattern described in this compose tutorial: https://youtu.be/SMOhl9RK0BA?t=546 : don't pass five different text with five onClickLambdas for five different buttons to a component, just pass in a list of 5 Buttons
For higher level components i like to make sure i only pass high level stuff, and let the composable figure out the low level things.

Related

Compose: Proper way of extending the RowScope Modifier? (Or any Scope Modifier for that matter)

I'm creating a Row of Text Composables, imagine it like a table row.
Image: https://i.stack.imgur.com/jR3oB.png
If you care for the backstory:
The amount of "cells" of my custom Row Composable is dynamic and I use Box Composables to wrap the Text in the "cells". I need to apply different Modifier parameters to each Box, which I first tried to achieve via creating a Modifier extension.
Code of my Modifier extension:
#SuppressLint("ComposableModifierFactory", "ModifierFactoryExtensionFunction") // I wish I could. The RowScope requirement makes doing it the easy way impossible
#Composable
fun RowScope.rowOfTextCellsBoxModifier(
index: Int,
columnWidths: ArrayList<Dp?>,
columnWeights: ArrayList<Float?>
): Modifier {
#Suppress("RedundantExplicitType") // bull**** (compose kotlinCompilerExtensionVersion 1.3.2)
var returnModifier: Modifier = Modifier
if (index != columnWidths.size - 1)
returnModifier = returnModifier.then(Modifier.verticalEndLine())
returnModifier = if (columnWidths[index] != null)
returnModifier.then(Modifier.width(columnWidths[index]!!))
else {
val weight = try {
columnWeights[index]
} catch (ignored: Throwable) {
1f
}
returnModifier.then(Modifier.weight(weight!!))
}
return returnModifier
}
I moved on to just generating the entire Box in a #Composable function, but that's beside the point ;)
As you can see, I want to add a Modifier.weight() at one point.
However, .weight() is a Modifier function that is only applicable in certain Scopes, like in RowScope. Which means that my "Modifier extension" needs to apply to RowScope, which turned out impossible to find instructions on.
So, imagine I hadn't found a better solution:
What would be the proper way of extending the RowScope Interface's Modifier?
Usually, when you need to create a Composable that is applicable to certain scopes, you write something like:
RowScope.YourComposable() {...}
And when you extend Modifier, you write:
Modifier.yourExtension() {...}
But that does not work for Scoped Modifiers:
RowScope.Modifier.YourModifierExtension() {...}
is invalid code.

How to use remember & mutableStateOf seperately?

Usually, compose codes are like
#Preview
#Composable
fun BuildMyView() {
val counter = rememberSaveable { mutableStateOf(1) }
Text(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(align = Alignment.Center)
.clickable { counter.value++ },// click and text will ++
text = "${counter.value}"
)
}
Recently, I want to collect all view data together and build a state and create sth like:
data class MyState(
val data: MutableState<String>
)
val stateTemp = MyState(mutableStateOf("hello"))
#Preview
#Composable
fun BuildMyView() {
val counter = rememberSaveable { stateTemp.data}
Text(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(align = Alignment.Center)
.clickable { stateTemp.data.value += "-1" },
text = counter.value
)
}
In second case, when I click text, it does ++ but if I go to a new page and come back, all changes will lost however in first case it doesn't.
I then read some compose codes and get confusing since I didn't find where remember subscribe a mutable state.
Is there a method to make mutable state out of compose work?
Beside, is somewhere I can find codes generated by compose under my gradle build dir or anywhere else (except dex, that's too hard to read)? Compose really did amazing job but I cannot read the real codes running and that makes much more difficult for freshman to get start.
UPDATE ON 2022/6/15
Now I found a proper solution to use viewmodel instead of state holder and use mutableState in viewmodel and subscribe state in view composable part so that I can avoid complex grammar of viewmodel with livedata. Hope that will help followers.
I then read some compose codes and get confusing since I didn't find where remember subscribe a mutable state.
As far as I understand, remember calculates and caches what ever is inside its lambda during the first composition or first execution of the #composable function only, this is the only thing that the #composable remembers or subscribes on. Unless you provide a key to a remember, that when it changed, it will trigger a re-calculation to be remembered
Is there a method to make mutable state out of compose work?
Do you mean some piece of non composable code or function that will automatically trigger when this State object is changed? If this is what you mean, I suppose you just have to resort back to good old observable patterns
As far as I understand, compose States are specifically designed to work with the underlying Snapshot system where the heart of triggering the composition mechanism happens, I can't imagine (so far) any usage of State objects outside of composition or outside of the Snapshot system.
In second case, when I click text, it does ++ but if I go to a new page and come back, all changes will lost however in first case it doesn't.
As for using rememberSaveable, I would be careful using it , it might look the same as remember with just an additional power of saving/restoration, but it has more power with equal responsibilities imposed to it that you have to take into account when using it. I haven't used or defined a rememberSaveable object without defining a Saver in it explicitly, so I can only assume in your case here
rememberSaveable { mutableStateOf(1) }
the composable observes a rememberSaveable that also observes an actual object that actually changes and implicitly saves and restores that object with the most recent value
while in this, I think
data class MyState(
val data: MutableState<String>
)
val stateTemp = MyState(mutableStateOf("hello"))
...
...
rememberSaveable { stateTemp.data}
rememberSaveable saves the state of the val data: MutableState<String> (which is empty), while the composable observes an instance of a mutableState that is changing, unfortunately rememberSaveable already saved an initial state in way like this rememberSaveable { data: MutableState<String> } not the actual mutableStateOf("hello") that changes, so it will restore it that way when you go back.
I'm curious, you can try implementing your MyState class with a companion object holding a Saver where you can define how rememberSaveable will Save and Restore the data, I think it will restore it when you navigate back to that screen. When you debug a Saver implementation, you will also notice that the restoration is invoked during recomposition. Im not quite sure though
Disclaimer: Im just learning compose recently and still digging deeper about the Snapshot system, and it seemed like you're heading in the same direction as I do. I'd recommend to visit this link once in a while if your'e interested in the Snapshot system. Apologies as well, I can't comment yet due to lack of reputation so I just posted my thought and current understanding of how State and composition work together.

FP patterns for combining data from different sources (preferrably in Kotlin and Arrow)

Disclaimer upfront: Recently, my interest in functional programming has grown and I've been able to apply the most basic approaches (using pure functions as much as my knowledge and working environment permits) in my job. However, I'm still very inexperienced when it comes to more advanced techniques and I thought trying to learn some by asking a question on this site might be the right idea. I've stumbled over similar issues once every while, so I think there should be patterns in FP to deal with this type of problems.
Problem description
It boils down to the following. Suppose there is an API somewhere providing a list of all possible pets.
data class Pet(val name: String, val isFavorite: Boolean = false)
fun fetchAllPetsFromApi(): List<Pet> {
// this would call a real API irl
return listOf(Pet("Dog"), Pet("Cat"), Pet("Parrot"))
}
This API knows nothing about the "favorite" field and it shouldn't. It's not under my control. It's basically just returning a list of pets. Now I want to allow users to mark pets as their favorite. And I store this flag in a local database.
So after fetching all pets from the api, I have to set the favorite flag according to the persisted data.
class FavoriteRepository {
fun petsWithUserFavoriteFlag(allPets: List<Pet>) {
return allPets.map { it.copy(isFavorite = getFavoriteFlagFromDbFor(it) }
}
fun markPetAsFavorite(pet: Pet) {
// persist to db ...
}
fun getFavoriteFlagFromDbFor(pet: Pet): Boolean {...}
}
For some reason, I think this code dealing with the problem of "fetch one part of the information from one data source, then merge it with some information from another" might benefit from the application of FP patterns, but I'm not really sure in which direction to look.
I've read some of the documentation of Arrow (great project btw :)) and am quite a Kotlin enthusiast, so answers utilizing this library would be very appreciated.
Here's something I'd potentially do. Your code has a couple of important flaws that make it unsafe from the functional programming perspective:
It doesn't flag side effects, so compiler is not aware of those and cannot track how they're are used. That means we could call those effects from anywhere without any sort of control. Examples of effects would be the network query or all the operations using the database.
Your operations don't make explicit the fact that they might succeed or fail, so callers are left to try / catch exceptions or the program will blow up. So, there's not a strong requirement to handle both scenarios, which could drive to missing some exceptions and therefore get runtime errors.
Let's try to fix it. Let's start by modeling our domain errors so we have a set of expected errors that our domain understands. Let's also create a mapper so we map all potential exceptions thrown to one of those expected domain errors, so our business logic can react to those accordingly.
sealed class Error {
object Error1 : Error()
object Error2 : Error()
object Error3 : Error()
}
// Stubbed
fun Throwable.toDomainError() = Error.Error1
As you see, we're stubbing the errors and the mapper. You can put time on designing what errors you'll need for your domain on an architecture level and write a proper pure mapper for those. Let's keep going.
Time for flagging our effects to make the compiler aware of those. To do that we use suspend in Kotlin. suspend enforces a calling context at compile time, so you cannot ever call the effect unless you're within a suspended environment or the integration point (a couroutine). We are going to flag as suspend all operations that would be a side effect here: the network request and all db operations.
I'm also taking the freedom to pull out all DB operations to a Database collaborator just for readability.
suspend fun fetchAllPetsFromApi(): List<Pet> = ...
class FavoriteRepository(private val db: Database = Database()) {
suspend fun petsWithUserFavoriteFlag(allPets: List<Pet>) {
... will delegate in the Database ops
}
}
class Database {
// This would flag it as fav on the corresponding table
suspend fun markPetAsFavorite(pet: Pet): Pet = ...
// This would get the flag from the corresponding table
suspend fun getFavoriteFlagFromDbFor(pet: Pet) = ...
}
Our side effects are safe now. They've become description of effects instead, since we cannot ever run them without providing an environment capable of running suspended effects (a coroutine or another suspended function). In functional jargon we'd say our effects are pure now.
Now, let's go for the second issue.
We also said that we were not making explicit the fact that each effect might succeed or fail, so callers might miss potential exceptions thrown and get the program blown up. We can raise that concern over our data by wrapping it with the functional Either<A, B> data type. Let's combine both ideas together:
suspend fun fetchAllPetsFromApi(): Either<Error, List<Pet>> = ...
class FavoriteRepository(private val db: Database = Database()) {
suspend fun petsWithUserFavoriteFlag(allPets: List<Pet>): Either<Error, List<Pet>> {
... will delegate in the Database ops
}
}
class Database {
// This would flag it as fav on the corresponding table
suspend fun markPetAsFavorite(pet: Pet): Either<Error, Pet> = ...
// This would get the flag from the corresponding table
suspend fun getFavoriteFlagFromDbFor(pet: Pet): Either<Error, Boolean> = ...
}
Now this makes explicit the fact that each one of those computations might succeed or fail, so the caller will be forced to handle both sides and will not forget about handling the potential errors. We're using the types in our benefit here.
Let's add the logics for the effects now:
// Stubbing a list of pets but you'd have your network request within the catch block
suspend fun fetchAllPetsFromApi(): Either<Error, List<Pet>> =
Either.catch { listOf(Pet("Dog"), Pet("Cat")) }.mapLeft { it.toDomainError() }
We can use Either#catch to wrap any suspended effects that might throw. This automatically wraps the result into Either so we can keep computing over it.
More specifically, it wraps the result of the block in Either.Right in case it succeeds, or the exception into Either.Left in case it throws. We also have mapLeft to map potential exceptions thrown (Left side) to one of our strongly typed domain errors. That is why it returns Either<Error, List<Pet>> instead of Either<Throwable, List<Pet>>.
Note that with Either we always model errors on the left side. This is by convention, since Right represents the happy path and we want our successful data there, so we can keep computing over it with map, flatMap, or whatever.
We can apply the same idea for our db methods now:
class Database {
// This would flag it as fav on the corresponding table, I'm stubbing it here for the example.
suspend fun markPetAsFavorite(pet: Pet): Either<Error, Pet> =
Either.catch { pet }.mapLeft { it.toDomainError() }
// This would get the flag from the corresponding table, I'm stubbing it here for the example.
suspend fun getFavoriteFlagFromDbFor(pet: Pet): Either<Error, Boolean> =
Either.catch { true }.mapLeft { it.toDomainError() }
}
We're stubbing the results again, but you can imagine we'd have our actual suspended effects loading from or updating the DB tables inside each Either.catch {} block above.
Finally, we can add some logic to the repo:
class FavoriteRepository(private val db: Database = Database()) {
suspend fun petsWithUserFavoriteFlag(allPets: List<Pet>): Either<Error, List<Pet>> =
allPets.map { pet ->
db.getFavoriteFlagFromDbFor(pet).map { isFavInDb ->
pet.copy(isFavorite = isFavInDb)
}
}.sequence(Either.applicative()).fix().map { it.toList() }
}
Ok this one might be a bit more complex due to how our effects are written, but I'll try to make it clear.
We need to map the list so for each pet loaded from network we can load its fav state from the Database. Then we copy it as you were doing. But given getFavoriteFlagFromDbFor(pet) returns Either<Error, Booelan> now we'd have a List<Either<Error, Pet>> as a result 🤔 That might make it hard to work with the complete list of pets, since we'd need to iterate and for each one first we'd need to check whether it's Left or Right.
To make it easier to consume the List<Pet> as a whole, we might want to swap the types here, so we'd have Either<Error, List<Pet>> instead.
To this magic, one option would be sequence. sequence requires the Either applicative in this case since that'll be used to lift the intermediate results and the final list into Either.
We're also using the chance to map the ListK into the stdlib List instead, since ListK is what sequence uses internally, but we can understand it as a functional wrapped over List in broad words, so you have an idea. Since here we're only interested on the actual list to match our types, we can map the Right<ListK<Pet>> to Right<List<Pet>>.
Finally, we can go ahead and consume this suspended program:
suspend fun main() {
val repo = FavoriteRepository()
val hydratedPets = fetchAllPetsFromApi().flatMap { pets -> repo.petsWithUserFavoriteFlag(pets) }
hydratedPets.fold(
ifLeft = { error -> println(error) },
ifRight = { pets -> println(pets) }
)
}
We're going for flatMap since we have sequential ops here.
There are potential optimizations we could do like using parTraverse to load all the fav states from DB for a list of pets in parallel and gather results in the end, but I didn't use it since I'm not sure your database is prepared for concurrent access.
Here's how you could do it:
suspend fun petsWithUserFavoriteFlag(allPets: List<Pet>): Either<Error, List<Pet>> =
allPets.parTraverse { pet ->
db.getFavoriteFlagFromDbFor(pet).map { isFavInDb ->
pet.copy(isFavorite = isFavInDb)
}
}.sequence(Either.applicative()).fix().map { it.toList() }
I think we could also simplify the whole thing a bit more by changing some of the types and how operations are structured but wasn't sure about refactoring it too much from your codebase since I'm not aware of your current team constraints.
And here's the complete codebase:
import arrow.core.Either
import arrow.core.extensions.either.applicative.applicative
import arrow.core.extensions.list.traverse.sequence
import arrow.core.extensions.listk.foldable.toList
import arrow.core.fix
import arrow.core.flatMap
data class Pet(val name: String, val isFavorite: Boolean = false)
// Our sealed hierarchy of potential errors our domain understands
sealed class Error {
object Error1 : Error()
object Error2 : Error()
object Error3 : Error()
}
// Stubbed, would be a mapper from throwable to any of the expected domain errors used via mapLeft.
fun Throwable.toDomainError() = Error.Error1
// This would call a real API irl, stubbed here for the example.
suspend fun fetchAllPetsFromApi(): Either<Error, List<Pet>> =
Either.catch { listOf(Pet("Dog"), Pet("Cat")) }.mapLeft { it.toDomainError() }
class FavoriteRepository(private val db: Database = Database()) {
suspend fun petsWithUserFavoriteFlag(allPets: List<Pet>): Either<Error, List<Pet>> =
allPets.map { pet ->
db.getFavoriteFlagFromDbFor(pet).map { isFavInDb ->
pet.copy(isFavorite = isFavInDb)
}
}.sequence(Either.applicative()).fix().map { it.toList() }
}
class Database {
// This would flag it as fav on the corresponding table, I'm stubbing it here for the example.
suspend fun markPetAsFavorite(pet: Pet): Either<Error, Pet> =
Either.catch { pet }.mapLeft { it.toDomainError() }
// This would get the flag from the corresponding table, I'm stubbing it here for the example.
suspend fun getFavoriteFlagFromDbFor(pet: Pet): Either<Error, Boolean> =
Either.catch { true }.mapLeft { it.toDomainError() }
}
suspend fun main() {
val repo = FavoriteRepository()
val hydratedPets = fetchAllPetsFromApi().flatMap { pets -> repo.petsWithUserFavoriteFlag(pets) }
hydratedPets.fold(
ifLeft = { error -> println(error) },
ifRight = { pets -> println(pets) }
)
}

Usage of Dagger2 outside Android

I've recently started to learn Dagger. In order to do that, i've decided to write a simple console application to get the feeling of how various dagger features (like modules, component, subcomponents and component dependencies) fit together in an app architecture. As I don't really understeand it and given how hard it is to find an application sample created with dagger2 which is not Android app, i've decided to open a question here.
The first and probably most important question is: is dagger2 even ment to be used outside android?
If yes, then lets consider a simple application architecture: we have the data layer, service layer and ui layer
Data layer might consist of some kind of facade:
(Following code snippets will be written in Kotlin)
class Entity(var id: Int)
interface Repository {
fun findEntityById(id: Int): Entity?
fun deleteEntity(entity: Entity): Boolean
fun saveEntity(entity: Entity): Boolean
fun findAllEntities(): List<Entity>
}
Then i could have a couple of implementations of this facade:
class InMemoryRepository #Inject constructor() : Repository {
private val entities: MutableList<Entity> = LinkedList()
override fun findEntityById(id: Int): Entity? = entities.firstOrNull { it.id == id }
override fun deleteEntity(entity: Entity) = entities.remove(entity)
override fun saveEntity(entity: Entity) = entities.add(entity)
override fun findAllEntities(): List<Entity> = LinkedList(entities)
}
For which i would have modules:
#Module
interface InMemoryPersistenceModule {
#Singleton
#Binds
fun bindRepository(rep: InMemoryRepository): Repository
}
Service layer would be simpler:
#Singleton
class Service #Inject constructor(repository: Repository) {
fun doSomeStuffToEntity(entity: Entity) {}
}
#Singleton
class AnotherService #Inject constructor(repository: Repository) {
fun doSomeStuffToEntity(entity: Entity) {}
}
But it gets a little bit unlcear when it comes to the UI layer. Lets say i have some kind of android-like activity:
interface Activity : Runnable
And some kind of class that manages those activities:
class UserInterfaceManager {
val activityStack: Stack<Activity> = Stack()
val eventQueue: Queue<Runnable> = LinkedList()
fun startActivity(activity: Activity) = postRunnable {
activityStack.push(activity)
activity.run()
}
fun postRunnable(callback: () -> Unit) = eventQueue.add(callback)
fun stopActivity() { TODO() }
//other
}
How does dagger fit into this scenario? The articles i have read about the the dagger with android suggest createing the application component to inject my activites:
#Singleton
#Component(modules = [InMemoryPersistenceModule::class])
interface ApplicationComponent {
fun injectSomeActivity(activity: SomeActivity)
// and more
}
But then, where would the injection go to? It does't really make sense to put it in the UserInterfaceManager as Activities will most likely need an instance of it, which would create a circular dependency.
I also do not like the idea of the component being obtained from some kind of static method/property and injecting the activity from inside of it at the startup, as it creates duplicate lines of code in each activity.
Also, where do components and subcomponents fit in this kind of architecture? Why not create the separate
component for the data layer and expose just the repository and declare it as a dependency of the app component which would further isolate the details from abstraction? Maybe i should declare this component a dependcy of a service component which would enforce the layer architecure, as components can only use the types exposed in component interface? Or maybe i should use compoenent only when i need a custom scope and use the modules everywhere elsewhere?
I just overally think I am missing the bigger picture of the dagger. I will be really greatefull for answers, explanations and links to articles and other resouces that will let me understeand it better.
From the perspective of an Android developer, I fully understand your point. I asked myself this question too. The way how you construct an object in plain Java/Kotlin world is a little bit different. The main reason is due to the fact basic Android components (Activity/Fragment) don't allow constructor injection.
The answer to your question is, though, pretty straightforward. The Dagger Component is responsible for object creation, and you, as a developer, control what objects specific component provides. Let's use it in your scenario and provide some of the objects you might be interested in:
#Singleton
#Component(modules = [InMemoryPersistenceModule::class])
interface ApplicationComponent {
val service: Service
val anotherService: AnotherService
}
ApplicationComponent should be understood as a component for your whole application. It's not related to Android's Application class in any way. Now, you can simply create your component and let Dagger instantiate your objects:
val component = DaggerApplicationComponent.create()
val anotherService: AnotherService = component.anotherService
val service: AnotherService = component.service

How to dynamically chose a transform function based on current observable in project reactor?

Hello dear reactive programmers, I started to learn project reactor but I still struggle to figure out what operator to use when. I figured out, that if I want to have reusable parts to define a reactor flow, I can use the transform operator. What I would like to achieve is to use a certain implementation of such a flow function based on the current observables context. For a Mono flow, I came up with this, but I am very unsure, if it is a good solution:
So here is a part of the flow
class CloudeventOverDelegatorRoute(
val fromHttpToDelegatorRoute: FromHttpToDelegatorRoute,
val delegatorProvider: DelegatorProvider,
val fromDelegatorToHttpRoute: FromDelegatorToHttpRoute
): MessageRoute<HttpBaseMessage, HttpResponseMessage> {
override fun isHandlerFor(context: RouteContext): Boolean {
return fromHttpToDelegatorRoute.isHandlerFor(context)
&& fromDelegatorToHttpRoute.isHandlerFor(context)
}
override fun buildPipeline(input: Mono<RoutableMessage<HttpBaseMessage>>): Mono<RoutableMessage<HttpResponseMessage>> {
var dynamicallyDeterminedDelegator: Delegator? = null
return input.transform {
fromHttpToDelegatorRoute.buildPipeline(input)
}.handle<RoutableMessage<InternalMessage>> { t, u ->
dynamicallyDeterminedDelegator = delegatorProvider.provideDelegatorFor(t.routeContext)
u.next(t)
u.complete()
}.transform {
dynamicallyDeterminedDelegator!!.sendDelegated(it)
}.transform { fromDelegatorToHttpRoute.buildPipeline(it) }
}
}
Here is the dynamic selection logic
interface DelegatorProvider {
fun provideDelegatorFor(context: RouteContext): Delegator
}
class FirstMatchDelegatorProvider(
private val delegators: List<Delegator>
): DelegatorProvider {
override fun provideDelegatorFor(context: RouteContext): Delegator {
return delegators.firstOrNull {
it.isHandlerFor(context)
}?: throw IllegalStateException("No Delegator route available for context: $context")
}
}
And this is the delegator providing an essential sub-part of the whole flow
interface Delegator {
fun isHandlerFor(context: RouteContext): Boolean
fun sendDelegated(input: Mono<RoutableMessage<InternalMessage>>): Mono<RoutableMessage<InternalStatusMessage>>
}
What do you think? How would you solve it?
this approach is problematic because it relies on shared state (the dynamicallyDeterminedDelegator variable). If multiple subscribers subscribe to the returned Mono, they could overwrite each other delegator. Maybe that (multiple subscriptions) can't happen in your application, but this is a very bad habit to get into in any case.
looks like you can derive a delegator out of a RoutableMessage<InternalMessage> , and that you don't really need to retain that delegator.
the easiest way to resolve and apply the delegator to the routableMessage in one go is simply to use flatMap. see the (pseudo) java code below:
.flatMap(routableMessage -> {
val delegator = delegatorProvider.provideDelegatorFor(routableMessage.routeContext);
return delegator.sendDelegated(routableMessage);
})