Search code examples
androidkotlingenericsmvvmviewmodel

Kotlin generic in BaseClass. Trying to get ViewModel by generic type in BaseFragment


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?


Solution

  • 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