Always Learner
Always Learner

Reputation: 2962

How to use generics for ViewModel in Kotlin?

I want to create a MainFragment for my existing fragments and create a ViewModel object for each fragment that is provided by viewModels<FragmentName::class>() like this:

class MainFragment<VM: ViewModel>: Fragment() {
    private val viewModel by viewModels<VM::class>()
}

But I get this error:

Cannot use 'VM' as reified type parameter. Use a class instead.

This is what I want to have:

class ProfileFragment: MainFragment<ProfileViewModel>() {}

And simply use viewModel object from the parent class.

How to solve this?

Upvotes: 1

Views: 2861

Answers (4)

Muhammad Ahmed
Muhammad Ahmed

Reputation: 1048

Make your MainFragment abstract

abstract class MainFragment<VM: ViewModel>: Fragment() {
       abstract private val viewModel : VM
    }

Override in your child fragments

class ProfileFragment: MainFragment<ProfileViewModel>() {
    private val mViewModel by viewModels<YourViewModel>()
    override val viewModel get() = mViewModel

}    

Upvotes: 0

Amin
Amin

Reputation: 3186

You cannot use normal generic arguments like reified ones from inline functions (VM::class). But if you want to free yourself from writing by viewModels() for each fragment, you can use a dirty workaround to instantiate the viewModel from its Generic class.

But before I start, it's worth mentioning that viewModels<>() is an inline function which lazily creates your viewModels through ViewModelProvider(store).get(vmClass). So, if we can extract the Java Class of our viewModel from our parameterized (generic) Fragment class, we can get our viewModel using it.

In the simplest implementation, we can assume that there are no inheritance in our fragments other than BaseFragment (which is 99% of cases). We will get genericSuperclass which will represent the actual type parameters (the ViewModel class we were looking for) in its actualTypeParameters and then we instantiate the viewModel using the very first element

abstract class BaseFragment<VM : ViewModel> : Fragment() {
    lateinit var viewModel: VM
    private set

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

        // e.g. we are ProfileFragment<ProfileVM>, get my genericSuperclass which is BaseFragment<ProfileVM>
        // Actually ParameterizedType will give us actual type parameters
        val parameterizedType = javaClass.genericSuperclass as? ParameterizedType

        // now get first actual class, which is the class of VM (ProfileVM in this case)
        @Suppress("UNCHECKED_CAST")
        val vmClass = parameterizedType?.actualTypeArguments?.getOrNull(0) as? Class<VM>?

        if(vmClass != null)
            viewModel = ViewModelProvider(this).get( vmClass )
        else
            Log.i("BaseFragment", "could not find VM class for $this")
    }
}
class ProfileVM : ViewModel(){
    var x = 1
}

class ProfileFragment : BaseFragment<ProfileVM>() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        Log.i("ProfileFragment", "vm.x: ${viewModel.x}")
        return super.onCreateView(inflater, container, savedInstanceState)
    }
}

In addition, if you want to support inheritance and complex hierarchies, you can find the BaseFragment using superClass which I will add as another answer because I want to keep this answer clean and tidy :D

PS: I do not recommend what you're looking for, because if you want to create some fragments which only need a sharedViewModel a.k.a. activityViewModel() you have to add some more complex logic to this or deal with the duality of creating SOME viewModels manually, while this magic code will instantiate the rest for you!

Upvotes: 1

Amin
Amin

Reputation: 3186

In addition to my previous answer, there is a more dirty (but more complex) method, which you can use. You can iterate over all the hierarchy and find a type argument which is assignable from ViewModel. In this approach, we iterate every single super until we can find something which is assignable from ViewModel.

First, we check if the current type we are dealing with has generic type arguments that is a ViewModel or not, if we've found it, return it as an answer. Otherwise, repeat the same logic for the superClass.

fun<CLS> Class<*>.findGenericWithType(targetClass: Class<*>) : Class<out CLS>?{
    var currentType: Type? = this

    while(true){
        val answerClass = (currentType as? ParameterizedType)?.actualTypeArguments //get current arguments
            ?.mapNotNull { it as? Class<*> } //cast them to class
            ?.findLast { targetClass.isAssignableFrom(it) } // check if it is a target (ViewModel for example)

        // We found a target (ViewModel)
        if(answerClass != null){
            @Suppress("UNCHECKED_CAST")
            return answerClass as Class<out CLS>?
        }

        currentType = when{
            currentType is Class<*> -> currentType.genericSuperclass // Not a ParameterizedType so go to parent
            currentType is ParameterizedType -> currentType.rawType // a parameterized type which we could't find any ViewModel yet, so the raw type (parent) should have it
            else -> return null //or throw an exception
        }
    }
}

abstract class BaseFragment<VM : ViewModel> : Fragment() {
    lateinit var viewModel: VM
    private set

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

        val vmClass = this.javaClass.findGenericWithType<VM>(ViewModel::class.java)

        if(vmClass != null)
            viewModel = ViewModelProvider(this).get( vmClass )
        else
            Log.i("BaseFragment", "could not find VM class for $this")
    }
}

And finally

class ProfileFragment : BaseFragment<ProfileVM>()

Upvotes: 0

Muhammad Danish
Muhammad Danish

Reputation: 47

I did similar approach for databinding have a look at it:

1st i created a abstract basefragment:

abstract class BaseFragment<Binding:ViewDataBinding>:Fragment() {

 protected abstract fun setLayout(inflater: LayoutInflater, container: ViewGroup?):Binding
}

after that i access this in below fragment:

class HomeFragment : BaseFragment<FragmentHomeBinding>() {


    override fun setLayout(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {

        return DataBindingUtil.inflate(inflater,R.layout.fragment_home,container,false)
    }
}

You can replace Viewbinding with viewmodel.

Upvotes: 0

Related Questions