Antonis Radz
Antonis Radz

Reputation: 3097

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?

Upvotes: 5

Views: 2027

Answers (3)

Antonis Radz
Antonis Radz

Reputation: 3097

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 = [email protected]
    }
}

Dirty, but saves tones of coding

Upvotes: 1

ianbelow
ianbelow

Reputation: 19

There is clearer solution:

abstract class BaseActivity<VM : BaseViewModel> : AppCompatActivity {

    protected val viewModel: VM by viewModel(clazz = getViewModelClass())

    private fun getViewModelClass(): KClass<VM> = (
    (javaClass.genericSuperclass as ParameterizedType)
        .actualTypeArguments[0] as Class<VM>
    ).kotlin
}

And usage:

class MainActivity : BaseActivity<MainViewModel>(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       viewModel.onViewCreated()
    }
}

Upvotes: 1

Antonis Radz
Antonis Radz

Reputation: 3097

Found working way, but is it clean enough?

abstract class BaseModelFragment<VM : ViewModel>(viewModelClass: KClass<VM>) : DaggerFragment() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    val viewModel by viewModel(viewModelClass) { viewModelFactory }

    private fun Fragment.viewModel(
        clazz: KClass<VM>,
        ownerProducer: () -> ViewModelStoreOwner = { this },
        factoryProducer: (() -> ViewModelProvider.Factory)? = null,
    ) = createViewModelLazy(clazz, { ownerProducer().viewModelStore }, factoryProducer)
}

And usage:

open class SomeFragment : BaseModelFragment<CustomerSupportViewModel>(CustomerSupportViewModel::class) {
...
}

It is tested and working. Any ideas how to improve it? :)

Upvotes: 2

Related Questions