arkivanov/MVIKotlin

Add composition for DSL API

SuLG-ik opened this issue · 1 comments

p.s. Early I make PR with same name, but now I would like to discuss about important of this API.

Reasons

  • to extend onIntent behavior should create own extension functions, that's easy with basic functional (like skips intent's when previous still is in execution with async), but hard with more difficult functional (like add multiple intent's with disabling/enabling with feature toggle and merge it with some another behavior like skipping)
  • no abilities to change behavior for multiple registered intents, now executes only first

What's new

  • ExecutionHandler (might be named as ExecutionChain, ExecutionProvider) with method handle(Scope, Intent/Action): Boolean
  • common ExecutorDSL that has methods onIntent/onAction to store ExecutionHandlers
  • abstract DslExecutorImpl that execute ExecutionHandlers on intents/actions

Solution

ExecutionHandlers flow

DirectExecutionHandler (just call provided lambda with scope, always true) <=

<= .... some user's handlers or nothing <=

<= TypedExecutionHandler (compare type of executed intent and call execution handler above if matches and return true, or if not false) <=
<= MultipleExecutionHandler (always false, that's understood by executor as continue calling next registered handlers) or SingleExecutionHandler (return status by handlers above, for example TypedExecutionHandler does not matches types or other user's provided handlers does not execute handler)

User's usage

Each ExecutionHandler, exclude DirectExecutionHandler, should has next handler to call it if some condition

CoroutineSkippingExecutionHandler example

class CoroutineSkippingExecutionHandler<T : Any, State : Any, Message : Any, Action : Any, Label : Any> @PublishedApi internal constructor(
    private val nextHandler: ExecutionHandler<T, CoroutineExecutorScope<State, Message, Action, Label>>,
) : ExecutionHandler<T, CoroutineExecutorScope<State, Message, Action, Label>> {

    private var jobHost: Job = Job()

    override fun handle(scope: CoroutineExecutorScope<State, Message, Action, Label>, value: T): Boolean {
        if (jobHost.children.any()) {
            return false
        }
        val newScope = CoroutineSkippingExecutorScope(jobHost, scope)
        nextHandler.handle(newScope, value)
        return true
    }

}

and usage-friendly creating for give to common ExecutorDsl's onIntent

inline fun <reified T : Any, Action : Any, State : Any, Message : Any, Label : Any> coroutineSkippingExecutionHandler(
    noinline handler: CoroutineExecutorScope<State, Message, Action, Label>.(T) -> Unit
): ExecutionHandler<T, CoroutineExecutorScope<State, Message, Action, Label>> {
    return singleExecutionHandler(typedExecutionHandler(CoroutineSkippingExecutionHandler(directExecutionHandler(handler))))
}

in Executor

coroutineExecutorFactory<Some, Nothing, Nothing, Nothing, Nothing> {
   onIntent(coroutineSkippingExecutionHandler { intent: Some.A ->
          /* something */
   })
}

or if add to base api of CoroutineExecutor

coroutineExecutorFactory<Some, Nothing, Nothing, Nothing, Nothing>(Dispatchers.Unconfined) {
   onIntentSkipping<Some.A> {
       /* something */
   }
}

About abilities for changing execution multiple/single strategy

enum class ExecutionStartegy {
   SINGLE, MuLTIPLE
}

currently exists onIntent method with new parameter in exists CoroutineExecutorBuilder

inline fun <reified T : Intent> onIntent(
   noinline handler: CoroutineExecutorScope<State, Message, Action, Label>.(intent: T) -> Unit,
   strategy: ExecutionStartegy = ExecutionStartegy.Single,
) {
   // logic matches new suggested api
   onIntent(coroutineExecutionHandler(handler, startegy)) // call common ExecutorBuilder's onIntent
}

and under the hood as bottom ExecutionHandlers flow use's ExecutionStrategyExecutionHandler

private fun <T : Any, Scope : Any> ExecutionStrategy.handler(nestedHandler: ExecutionHandler<T, Scope>): ExecutionHandler<T, Scope> {
    return when (this) {
        ExecutionStrategy.SINGLE -> singleExecutionHandler(nestedHandler)
        ExecutionStrategy.MULTIPLE -> multipleExecutionHandler(nestedHandler)
    }
}

class ExecutionStrategyExecutionHandler<T : Any, Scope : Any>(
    nestedHandler: ExecutionHandler<T, Scope>,
    strategy: ExecutionStrategy,
) : ExecutionHandler<T, Scope> {
    private val handler = strategy.handler(nestedHandler)
    override fun handle(scope: Scope, value: T): Boolean {
        return handler.handle(scope, value)
    }
}

Benefits

  • better control of execution by user
  • ability to composite execution behaviours
  • save exists API, onIntent/onAction with lambda of exists CoroutineExecutorBuilder/ReactiveExecutorBuilder still exists

Thanks for describing the request in details. The current Store DSL is primarily designed for simple cases. For more complex cases it's recommended to extend CoroutineExecutor. The proposed API will dramatically increase cognitive load for the developer when working with it.

If you really want such complex logic with Store DSL, feel free to create your API specifically for your needs.

Thanks again, but I don't think it's a good idea to bring this to MVIKotlin.