I'm looking for a way to pass configuration input to a factory which is derived from a base class and holding different input parameters depending on that derived class for that factory.
I'm struggling to find a good way to implement this. So let me show what I've currently got and where the problem is:
class ExampleFragmentFactoryImpl @Inject constructor(
private val providers: List<ExampleFragmentProvider<out ExampleInput>>
): ExampleFragmentFactory {
@Suppress("UNCHECKED_CAST")
override suspend fun <T: ExampleInput> create(
pageType: T
): Fragment {
providers.forEach { provider ->
try {
val typesafeProvider = provider as? ExampleFragmentProvider<T>
typesafeProvider?.let {
return it.provide(pageType)
}
} catch (e: ClassCastException) {
// This try-except-block shall be avoided.
}
}
throw IllegalStateException("could not create Fragment for pageType=$pageType")
}
}
Here the factory interface...
interface ExampleFragmentFactory {
suspend fun <T : ExampleInput> create(
pageType: T
): Fragment
}
Now the provider interface...
interface ExampleFragmentProvider<T: ExampleInput> {
suspend fun provide(
pageType: T
) : Fragment
}
the input class...
sealed class ExampleInput {
object NotFound : ExampleInput()
object WebView : ExampleInput()
data class Homepage(
val pageId: String
) : ExampleInput()
}
and finally a provider implementation:
internal class ExampleHomepageProvider @Inject constructor() :
ExampleFragmentProvider<ExampleInput.Homepage> {
override suspend fun provide(pageType: ExampleInput.Homepage): Fragment {
TODO()
}
}
As commented above, it's really bad that try-except is necessary in the factory. There should be nice way of how to achieve this without try-except. Unfortunately, due to type erasure, it's not possible to check the type before casting. Working with reified types afaik is not possible using polymorphic code.
Another possible solution could be to avoid using generics and casting to the required input type within the providers provide() method -- but that's not really nice, too.
Do you have any suggestions how I can improve this kind of factory?
To do this we need to acquire
KType/KClass/Classof relatedExampleInputof a provider. There is no direct and straightforward way to acquire it due to type erasure, but still there are some ways to get hold of it.Solution #1: capture within reified param
We could register providers one by one using a function with reified type. However, I guess this is not possible in your case as you use dependency injection to acquire providers.
Solution #2: provide by provider
We can make providers responsible for providing their related input types. This is pretty common solution in cases like this.
First, we create additional property in
ExampleFragmentProviderto expose its associatedTtype:Alternatively, we can use
KTypeorClasshere.Then, we use this exposed type/class to search for a matching provider in the factory:
Note that contrary to your original solution, it searches for the exact type. If your
ExampleInputhas deep subtypes structure, thenExampleHomepageProviderwon't be used when asked for e.g.ExampleInput.HomepageSubtype.Solution #3: reflection voodoo
Generally speaking, type parameters in Java/Kotlin are erased. However, in some cases they're still obtainable. For example,
ExampleHomepageProviderwas defined as a subtype ofExampleFragmentProvider<ExampleInput.Homepage>and this information is stored in the bytecode. So wouldn't it make sense to use this info to acquireT? Yes, it makes sense, and yes, it is possible with some crazy reflection voodoo:Then, we can use this function in the factory as a replacement of
inputType:Note, this is pretty advanced stuff and it is good to have some low-level understanding of generics in JVM. For example, if we create a generic provider then its
Tmay be actually erased for good and above function will throw an exception:Solution #4: 2 + 3 = 4
If we like to go with reflection voodoo, it probably makes sense to still make providers responsible for acquiring their
T. This is good for OOP and is more flexible as different providers could decide to use different ways to get their type. We can provide default implementation ofinputTypeat the interface and/or provide abstract implementation:There is important difference between them. Default implementation on interface has to calculate everything each time
inputTypeis accessed. Abstract class cachesinputTypewhen initializing.Of course, providers can still override the property and e.g. provide the type directly, as in earlier examples.