How to get generic parameter class in Kotlin
Asked Answered
W

6

123

Firebase's snapshot.getValue() expects to be called as follows:

snapshot?.getValue(Person::class.java)

However I would like to substitute Person with a generic parameter that is passed into the class via the class declaration i.e.

class DataQuery<T : IModel> 

and use that generic parameter to do something like this:

snapshot?.getValue(T::class.java)

but when I attempt that I get an error stating that

only classes can be used on the left-hand side of a class literal

Is it possible to provide a class constraint on the generic parameter like in C# or is there some other syntax I can use to get the type info for the generic param?

Wahkuna answered 6/12, 2015 at 20:40 Comment(2)
can you provide more context to your question, as a function with type parameter versus a class with type parameter would have two different answers. You accepted a link-only answer which covered one of those cases.Hypnotism
Your question should show the full context of the code causing the error. In some cases your code would be correct (if we imagine it is in an inline function with reified type), in other cases not. And there are two cases the generics could be but you don't make it clear.Hypnotism
H
120

For a class with generic parameter T, you cannot do this because you have no type information for T since the JVM erases the type information. Therefore code like this cannot work:

class Storage<T: Any> {
    val snapshot: Snapshot? = ...

    fun retrieveSomething(): T? {
        return snapshot?.getValue(T::class.java) // ERROR "only classes can be used..."
    }
}

But, you can make this work if the type of T is reified and used within an inline function:

class Storage {
    val snapshot: Snapshot? = ...

    inline fun <reified T: Any> retrieveSomething(): T? {
        return snapshot?.getValue(T::class.java)
    }
}

Note that the inline function if public can only access public members of the class. But you can have two variants of the function, one that receives a class parameter which is not inline and accesses private internals, and another inline helper function that does the reification from the inferred type parameter:

class Storage {
    private val snapshot: Snapshot? = ...

    fun <T: Any> retrieveSomething(ofClass: Class<T>): T? {
        return snapshot?.getValue(ofClass)
    }
    
    inline fun <reified T: Any> retrieveSomething(): T? {
        return retrieveSomething(T::class.java)
    }
}

You can also use KClass instead of Class so that callers that are Kotlin-only can just use MyClass::class instead of MyClass::class.java

If you want the class to cooperate with the inline method on the generics (meaning that class Storage only stores objects of type T):

class Storage <T: Any> {
    val snapshot: Snapshot? = ...

    inline fun <reified R: T> retrieveSomething(): R? {
        return snapshot?.getValue(R::class.java)
    }
}

The link to reified types in inline functions: https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters

Hypnotism answered 6/12, 2015 at 20:40 Comment(4)
I have class abstract class ViewModelFragment<T : ViewModel> : Fragment() and method inside ViewModelProviders .of(somethingNotInteresting) .get(getViewModelClass()) giving T.class:java from your code inline fun <reified R: T> getViewModelClass(): Class<R> = R::class.java, but Compiler complains that "Cannot use 'T' as reifined type. Use Class instead". Do you know how to fix it?Giantism
private inline fun <reified T> className() = T::class.java private fun initFragmentCommunication() { fragmentEventViewModel = ViewModelProviders.of(this, viewModelFactory).get(className<ActivityFragmentViewModel<ActivityFragmentEvents, String?>>()) }Tabulate
retrieveSomething can't be called from java thoughVitrify
@Vitrify correct, I was not confining my answer to a requirement that it be callable from Java since it was a Kotlin only question.Hypnotism
L
30

What you need is reified modifier for your generic param, you can read about it here. https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters So if you do something like that:

inline fun <reified T : Any>T.logTag() = T::class.java.simpleName

you will get name of the actual caller class, not "Object".

Lactoflavin answered 6/12, 2015 at 21:32 Comment(3)
Don't forget to declare your type parameter as T : Any, otherwise it will be nullable.Alica
Also, don't forget that the function has to be declared inline.Asternal
Thats actially doesnt work. You will receive not generic parameter class.Unsegregated
D
22

you can get its class type like this

snapshot?.getValue((this.javaClass
                        .genericSuperclass as ParameterizedType)
                        .actualTypeArguments[0] as Class<T>)
Defame answered 29/8, 2018 at 9:15 Comment(2)
getting error: java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedTypeBlacksnake
@SachidanandaSahu probably you are trying to cast the class itself (this.javaClass as ParameterizedType) or may be you have some inheritanceDefame
H
7

Using typeOf is another option. (Added in Kotlin 1.3)

Example:

@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> someMethod(data: T) {

    val typeClassifier = typeOf<T>().classifier

    if (typeClassifier == List::class) {
        //Do something
    }

}
Hughmanick answered 27/9, 2020 at 11:13 Comment(1)
This is actually a better option, as it also gives all the details about parameterized types of T, like if T is Map<String, List<Int>>, you can find out the entire thing - even the Int!Ophthalmologist
K
3
  1. Using class comparison. See https://mcmap.net/q/182419/-how-to-check-generic-type-in-kotlin.

     inline fun <reified T> SharedPreferences.getData(key: String, defValue: Any? = null): T? =
         when (T::class) {
             Integer::class -> {
                 val value = getInt(key, (defValue as? Int) ?: 0) as T
                 if (value == 0) defValue as? T else value
             }
             Long::class -> {
                 val value = getLong(key, (defValue as? Long) ?: 0L) as T
                 if (value == 0) defValue as? T else value
             }
             Boolean::class -> {
                 val value = getBoolean(key, (defValue as? Boolean) ?: false) as T
                 if (value == false) defValue as? T else value
             }
             String::class -> getString(key, defValue as? String) as T?
             else -> throw IllegalStateException("Unsupported type")
         }
    
  2. Using isAssignableFrom. See https://dev.to/cjbrooks12/kotlin-reified-generics-explained-3mie.

     inline fun <reified T : Number> SharedPreferences.getData(key: String): T? {
         val cls = T::class.java
         return if (cls.isAssignableFrom(Integer::class.java)) {
             getInt(key, 0) as T
         } else if (cls.isAssignableFrom(Long::class.java)) {
             getLong(key, 0) as T
         } else {
             throw IllegalStateException("Unsupported type")
         }
     }
    

For Double in SharedPreferences see https://mcmap.net/q/182420/-can-39-t-put-double-sharedpreferences.

Use:

val s: String? = preferences.getData("key", "")
val i = preferences.getData<Int>("key")
Kidron answered 18/7, 2019 at 9:44 Comment(0)
N
-2

This is what I have.

class ClubsViewModel<T>(clazz: Class<T>) :  BaseViewModel<T>(clazz) {
    private var _mClubs = MutableLiveData<List<T>>()

     listenToFireStoreCollection("Clubs", _mClubs)
...
}

class BViewModel<T>(clazz: Class<T>) :  BaseViewModel<T>(clazz) { 
  
    private var _mBs = MutableLiveData<List<T>>()
    listenToFireStoreCollection("Bname", _mBs)
...
}

class BaseViewModel<T>(val clazz: Class<T>) {
    protected val mFirestore = Firebase.firestore

    protected  fun listenToFireStoreCollection(val collectionName: String, liveData: MutableLiveData<List<T>>) 
        mFirestore.collection(collectionName).addSnapshotListener { snapshot, e ->
            if (e != null) {
                return@addSnapshotListener
            }
            if (snapshot != null) {
                liveData.value = snapshot.documents.mapNotNull { it.toObject(clazz) }
            }
        }
    }
}
//FRAGMENT EXAMPLES.
class ClubsFragment : Fragment() {

    private val mClubsViewModel: ClubsViewModel<ClubsFSEntity> by viewModels()
...
}

class BsFragment : Fragment() {

    private val mBsViewModel: BsViewModel<BsFSEntity> by viewModels()
...
}

I have a similar problem here: Hilt Dependency injection with Class<T> in ViewModel

Namhoi answered 8/4, 2021 at 20:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.