aryaxt aryaxt - 2 months ago 17
Android Question

Kotlin - Check to see if the generic parameter is optional or not?

I am writing this generic method to fetch data from firebase? In some cases getting null back is valid, in other cases is not, is it possible to check to see if the generic param is nullable or not?

ex

reference.obsrveObject(User.class)


should throw if null

reference.obsrveObject(User?.class)


should call onNext with null value

fun DatabaseReference.observeSingleEvent(): Observable<DataSnapshot?> {
return Observable.create { subscriber ->
val valueEventListener = object: ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot?) {
subscriber.onNext(snapshot)
subscriber.onCompleted()
}

override fun onCancelled(error: DatabaseError?) {
subscriber.onError(FirebaseDatabaseThrowable(error))
}
}

addListenerForSingleValueEvent(valueEventListener)
}
}

fun <T>DatabaseReference.obsrveObject(clazz: Class<T>): Observable<T> {
return observeSingleEvent().map { snapshot ->
if (snapshot != null) {
snapshot.getValue(clazz)
}
else {
// if clazz is nullable return null
// if clazz is not nullabel throw
throw Exception("")
}
}
}

Answer Source

Note: I've not personally used Firebase yet so some of the examples below may not compile but should be close enough.

Neither Class<T> nor KClass<T> track nullability as they only represent a class. A KType, however, can represent "a class with optional type arguments, plus nullability" and has an isMarkedNullable property.

You can use reified type parameters to get a KClass for the generic type but you cannot get (as of Kotlin 1.1) a KType. However, you can still check to see if the generic type is nullable (nullable reified type : Kotlin):

inline fun <reified T> isMarkedNullable(): Boolean {
    try {
        null as T
        return true
    } catch (e: TypeCastException) {
        return false
    }
}

You can then make calls like isMarkedNullable<User>() and isMarkedNullable<User?>() which return false and true respectively.


With this, as long as you can mark obsrveObject as inline, you can "check to see if the generic parameter is optional or not":

inline fun <reified T> DatabaseReference.obsrveObject(): Observable<T> {
    return observeSingleEvent().map { snapshot ->
        if (snapshot != null) {
            snapshot.getValue(T::class.java)
        } else if (isMarkedNullable<T>()) {
            null as T
        } else {
            throw Exception("")
        }
    }
}

Usage:

databaseReference.obsrveObject<User>() // Observable<User>
databaseReference.obsrveObject<User?>() // Observable<User?>

If you cannot use an inline function (and therefore reified type parameters) then you need to find a way to get a KType.

You can get a KType from the returnType on KCallable<R> but you can also create a KType from a KClass<T> using createType:

User::class.createType(nullable = false)    // User
User::class.createType(nullable = true)     // User?

The class here is the type's classifier so depending on how you are using obsrveObject you might change its argument type from Class<T> to KCallable<T>. You could change it to a KType directly and create instances as needed but I'm guessing your grabbing clazz currently from the return type of a property so I would go with KCallable<T>:

fun <T : Any> DatabaseReference.obsrveObject(callable: KCallable<T>): Observable<T?> {
    val kType = callable.returnType
    val kClass = kType.classifier as KClass<T>
    val clazz = kClass.java
    return observeSingleEvent().map { snapshot ->
        if (snapshot != null) {
            snapshot.getValue(clazz)
        } else if (kType.isMarkedNullable) {
            null
        } else {
            throw Exception("")
        }
    }
}

You would then call it using a reference to a callable (property, function, etc.):

databaseReference.obsrveObject(session::user)