Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice for using generics with Firebase snapshot.getValue()

TL;DR: How to correctly use a generic class with Firebase DataSnapshot.getValue()?

Use case: I want to implement a single generic remote datasource class for all my entities (a bunch of them) using Firebase. When listening to data change, I want to get the values from datasnapshot as an object of type E (its type determined elsewhere) but I don't know if it is doable with Firebase query, like the following:

public class GenRemoteDataSource<E extends SomeClass>
{
   //...
   public void onDataChange(DataSnapshot dataSnapshot)
   {
        E item = (E) dataSnapshot.getValue(); // <- unchecked cast and it doesn't work
        items.add(item);
   }
}

For example, I have a Foo class that extends SomeClass, and the implementation of this GenRemoteDataSource with a class of Foo would be:

public class Foo extends SomeClass{}

public class FooRemoteDataSource extends GenRemoteDataSource<Foo>
{
   //...
}

but Firebase throws a runtime error because instead of casting the getValue() as Foo, it would try to cast value as a upper bound SomeClass instead. I'm baffled as to why this happens:

Java.lang.ClassCastException: java.util.HashMap cannot be cast to com.example.app.SomeClass

please advise on how should I do this with Type-safety (No unchecked cast) . Thank you.

EDIT Stuff turned out to be irrelevant down below, see GenericTypeIndicator

EDIT I've also tried (blindly and heck worth a try) the GenericTypeIndicator,

GenericTypeIndicator<E> mTypeIndicator = new GenericTypeIndicator<>();
E item = dataSnapshot.getValue(mTypeIndicator);

but it instead spits the following runtime error.

com.google.firebase.database.DatabaseException: Not a direct subclass of GenericTypeIndicator: class java.lang.Object

like image 777
Andrew Lam Avatar asked Sep 15 '25 01:09

Andrew Lam


2 Answers

TL;DR my solution, (introduces more dependency than semantically necessary ?)

public class GenRemoteDataSource<E extends SomeClass>
{
   //...
   private final Class<E> clazz;

   GenRemoteDataSource(Class<E> clazz)
   {
      this.clazz = clazz;
   }

   //...
   public void onDataChange(DataSnapshot dataSnapshot)
   {
        E item = dataSnapshot.getValue(clazz); // <- now is type safe.
        items.add(item);
   }
}

I ended up just providing the Class definition to the getValue() via a constructor injection. This is done by passing a Class<T> into the generic class's constructor and sets it for Firebase's getValue() to use as a instance parameter. Please comment if there are better solution, because to me this seems redundant. Thank You!

like image 85
Andrew Lam Avatar answered Sep 16 '25 17:09

Andrew Lam


My solution in Kotlin for anybody interested:


class FirebaseParser<T>(val klass: Class<T>) {

    companion object {
        inline operator fun <reified T : Any> invoke() = FirebaseParser(T::class.java)
    }

    private fun checkType(t: Any): Boolean {
        return when {
            klass.isAssignableFrom(t.javaClass) -> true
            else -> false
        }

    }

    fun convert(data: DataSnapshot): T {

            val res = data.getValue() // Get out whatever is there.
                ?: throw MyCustomExceptionWhichIsHandledElseWhere("Data was null") // If its null throw exception

            if(checkType(res)) {
                return res as T //typecast is now safe
            } else {
                throw MyCustomExceptionWhichIsHandledElseWhere("Data was of wrong type. Expected: " + klass  + " but got: " + res.javaClass) // Data was the wrong type throw exception
            }
    }

}

Examples of instantiation

Here are some examples of instantiation of the class from java:

FirebaseParser<String> firebaseStringParser = new FirebaseParser<>(String.class);
FirebaseParser<Boolean> firebaseBooleanParser = new FirebaseParser<>(Boolean.class);


and from kotlin

val stringParser: FirebaseParser<String> = FirebaseParser(String::class.java)
val booleanParser: FirebaseParser<Boolean> = FirebaseParser(Boolean::class.javaObjectType) //javaObjectType so we do not compare Boolean to boolean without knowing

If doing it this way in Kotlin you need to make sure you not compare a prinmitive type to a class type without knowing

like image 21
Graunephar Avatar answered Sep 16 '25 16:09

Graunephar