116

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?

Jonik
  • 80,077
  • 70
  • 264
  • 372
Jaja Harris
  • 4,266
  • 4
  • 23
  • 22
  • 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. – Jayson Minard Dec 26 '15 at 17:05
  • 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. – Jayson Minard Dec 28 '15 at 00:20

6 Answers6

114

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

Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • 10
    I have class `abstract class ViewModelFragment : Fragment()` and method inside `ViewModelProviders .of(somethingNotInteresting) .get(getViewModelClass())` giving T.class:java from your code `inline fun getViewModelClass(): Class = R::class.java`, but Compiler complains that "Cannot use 'T' as reifined type. Use Class instead". Do you know how to fix it? – murt Nov 21 '17 at 14:25
  • private inline fun className() = T::class.java private fun initFragmentCommunication() { fragmentEventViewModel = ViewModelProviders.of(this, viewModelFactory).get(className>()) } – Ramesh Prasad Jun 25 '18 at 02:24
  • retrieveSomething can't be called from java though – anvarik Aug 23 '19 at 22:07
  • @anvarik correct, I was not confining my answer to a requirement that it be callable from Java since it was a Kotlin only question. – Jayson Minard Aug 24 '19 at 14:17
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".

Yaroslav
  • 4,750
  • 3
  • 22
  • 33
21

you can get its class type like this

snapshot?.getValue((this.javaClass
                        .genericSuperclass as ParameterizedType)
                        .actualTypeArguments[0] as Class<T>)
merdan
  • 1,229
  • 1
  • 11
  • 26
  • 2
    getting error: java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType – Sachidananda Sahu Dec 30 '19 at 13:17
  • @SachidanandaSahu probably you are trying to cast the class itself (this.javaClass as ParameterizedType) or may be you have some inheritance – merdan Jan 11 '20 at 09:22
6

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
    }

}
Ali Sadeghi
  • 397
  • 4
  • 16
  • This is actually a better option, as it also gives all the details about parameterized types of `T`, like if `T` is `Map>`, you can find out the entire thing - even the `Int`! – Simon Forsberg Mar 21 '23 at 11:30
5
  1. Using class comparison. See https://stackoverflow.com/a/61756761/2914140.

     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://stackoverflow.com/a/45412036/2914140.

Use:

val s: String? = preferences.getData("key", "")
val i = preferences.getData<Int>("key")
CoolMind
  • 26,736
  • 15
  • 188
  • 224
-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

nyxee
  • 2,773
  • 26
  • 22