Hey I want to create BaseFragment class that gets viewModel by generic type:
abstract class BaseFragment<B : ViewDataBinding, VM : ViewModel> : DaggerFragment() {
val viewModel by viewModels<VM> { viewModelFactory }
...
}
// Native function
@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
but getting error Cannot use 'VM' as reified type parameter. Use a class instead.
is it at all possible to achieve what I am trying to do? Maybe with other approach?
There dirty way to get ViewModel and ViewBinding only from generics:
abstract class BaseFragment<BINDING : ViewDataBinding, VM : ViewModel> : Fragment() {
val viewModel by viewModels(getGenericClassAt<VM>(1))
var binding: BINDING? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = inflater.inflateBindingByType<BINDING>(container, getGenericClassAt(0)).apply {
lifecycleOwner = this@BaseFragment
}.also {
it.onBindingCreated()
}
return binding?.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = false
internal open fun BINDING.onBindingCreated() {}
fun <T> withBinding(action: BINDING.() -> T): T? = binding?.let { action(it) }
}
@Suppress("UNCHECKED_CAST")
fun <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
((javaClass.genericSuperclass as? ParameterizedType)
?.actualTypeArguments?.getOrNull(position) as? Class<CLASS>)
?.kotlin
?: throw IllegalStateException("Can not find class from generic argument")
fun <BINDING : ViewBinding> LayoutInflater.inflateBindingByType(
container: ViewGroup?,
genericClassAt: KClass<BINDING>
): BINDING = try {
@Suppress("UNCHECKED_CAST")
genericClassAt.java.methods.first { inflateFun ->
inflateFun.parameterTypes.size == 3
&& inflateFun.parameterTypes.getOrNull(0) == LayoutInflater::class.java
&& inflateFun.parameterTypes.getOrNull(1) == ViewGroup::class.java
&& inflateFun.parameterTypes.getOrNull(2) == Boolean::class.java
}.invoke(null, this, container, false) as BINDING
} catch (exception: Exception) {
throw IllegalStateException("Can not inflate binding from generic")
}
And usage:
class BoardFragment : BaseFragment<FragmentBoardBinding, BoardViewModel>() {
override fun FragmentBoardBinding.onBindingCreated() {
viewModel = this@BoardFragment.viewModel
}
}
Dirty, but saves tones of coding