Search code examples
kotlingenericsinlineencapsulation

Encapsulation of inline reified method in kotlin


I have written the util class to be used with Flow<> in kotlin. The goal of the class is to simplify error handling and make possible to instantly finish the flow by calling breakFlow() method. In comparison to return@flow, this method can be called outside of the flow's context (for example inside the small private method that is then called in flow's builder).

My problem is that I use inline function withCatchingErrors() with reified type parameter. I don't want to expose this function outside of the FlowUtils class. Methods withBreakableFlow() should be exposed instead.

I can't get rid off inline function (or i don't know how to do it), because i need reified type parameter to check the type of data field inside AppErrorAsThrowable exception. Without <reified T> keyword i got error: Cannot check for instance of erased type: T?

There is the code:

class FlowUtils {
    class AppErrorAsThrowable private constructor(val error: AppError, val data: Any?) : Throwable() {
        companion object {
            fun create(error: AppError, data: Any?) = AppErrorAsThrowable(error, data)
        }
    }

    companion object {

        inline fun <reified T, R> FlowCollector<R>.withBreakableFlow(
            onBreakError: (breakError: Resource.Error<T>) -> Unit,
            flowImpl: () -> Unit
        ) {
            withCatchingErrors<T>(onBreakError, flowImpl)
        }

        fun <R> FlowCollector<R>.breakFlow(breakError: AppError, data: Any? = null): Nothing {
            throwError(breakError, data)
        }

        fun breakFlow(breakError: AppError, data: Any? = null): Nothing {
            throwError(breakError, data)
        }

        // TODO: it should be private, I don't want it to be exposed outside of this class
        inline fun <reified T> withCatchingErrors(
            onError: (error: Resource.Error<T>) -> Unit,
            flowImpl: () -> Unit
        ) {
            try {
                flowImpl()
            } catch (e: AppErrorAsThrowable) {
                if (e.data is T?) {
                    onError(Resource.Error(e.error, e.data as T?))
                } else {
                    Logger.wtf(
                        "FlowUtils::withCatchingErrors",
                        "wrong parameterized type inside the Resource.Error object that is a part of the Throwable thrown, it shouldn't happen at all!",
                        e
                    )
                    onError(Resource.Error(e.error))
                }
            }
        }

        private fun throwError(error: AppError, data: Any? = null): Nothing {
            throw AppErrorAsThrowable.create(error, data)
        }
    }
}

I have tried to make withCatchingErrors() private, but it makes me to make withBreakableFlow() method private too - which is not what i want obviously. withBreakableFlow() should be a public method.

The question is how to encapsulate method withCatchingErrors() without making withBreakableFlow() private?

Thank you in advance for any support.


Solution

  • First things first, if you can settle for internal visibility, you can make withCatchingErrors internal and use @PublishedApi to allow inline functions to use it despite its lower visibility.

    However, you have to understand that the function is still technically public and is inlined in the consumers' code, which means it is subject to binary compatibility requirements etc.

    It might be wiser to avoid @PublishedApi depending on your use case.

    Not all hope is lost, though. While you cannot cast or perform is checks on a generic T without reified, you can perform this kind of checks using reflection if you require a KClass<T> parameter (see KClass.isInstance).

    The general idea is to use inline+reified as a convenience on top of a publicly exposed API using KClass. Then your inline overload with reified type parameter can get the class using T::class (thanks to the reification) and then delegate to the overload taking the KClass.

    In your case, that could translate to something like this:

    inline fun <reified E : Any, R> FlowCollector<R>.withBreakableFlow(
        noinline onBreakError: (breakError: Resource.Error<E>) -> Unit,
        noinline flowImpl: () -> Unit
    ) {
        withBreakableFlow(E::class, onBreakError, flowImpl)
    }
    
    fun <E : Any, R> FlowCollector<R>.withBreakableFlow(
        errorType: KClass<E>,
        onBreakError: (breakError: Resource.Error<E>) -> Unit,
        flowImpl: () -> Unit
    ) {
        withCatchingErrors(errorType, onBreakError, flowImpl)
    }
    
    private fun <E : Any> withCatchingErrors(
        errorType: KClass<E>,
        onError: (error: Resource.Error<E>) -> Unit,
        flowImpl: () -> Unit
    ) {
        try {
            flowImpl()
        } catch (e: AppErrorAsThrowable) {
            if (errorType.isInstance(e.data)) {
                onError(Resource.Error(e.error, e.data as E?))
            } else {
                Logger.wtf(
                    "FlowUtils::withCatchingErrors",
                    "wrong parameterized type inside the Resource.Error object that is a part of the Throwable thrown, it shouldn't happen at all!",
                    e
                )
                onError(Resource.Error(e.error))
            }
        }
    }