You can perform Firestore read/write using custom objects instead of map.
class Quote( @get:Exclude override var id : String? = null, @JvmField @PropertyName(CREATED) @ServerTimestamp val created: Timestamp? = null, @JvmField @PropertyName(CONTENT) var content: String? = "", @JvmField @PropertyName(LIKE_COUNT) val likeCount: Long? = 0, @JvmField @PropertyName(IS_ACTIVE) val isAcive: Boolean? = true companion object { const val COLLECTION_NAME = "quote" const val CREATED = "created" const val CONTENT = "content" const val LIKE_COUNT = "like_count" const val IS_ACTIVE = "is_active" }}
Get Firestore document as custom object.
FirebaseFirestore.getInstance().collection(Quote.COLLECTION_NAME) .document("document_id") .get() .addOnSuccessListener { documentSnapshot -> val item = documentSnapshot.toObject(Quote::class.java) }
Tip 1: make all property nullable
I think it is a good pratice to make all property nullable (even for Int
, Long
and Boolean
). If the property for object class is not nullable and the actual value is null, calling toObject will cause java.lang.RuntimeException: Could not deserialize object.
.
NOTE: Null
value might be set accidentally by other clients.
NOTE: If the property is not defined in Firestore, there is no issue.
Tip 2: don't change underlying data type
We are using Boolean
for is_active
property. If you accidentally changed is_active
to Long
in Firestore, toObject
will cause
java.lang.RuntimeException: Could not deserialize object. Failed to convert a value of type java.lang.Boolean to int (found in field 'is_active')
When I bump into this issue, I try to edit Firestore using the admin console to fix is_active
back to Boolean
. Sadly, the exception still persisted on the Android client, probably because query take priority of local cache (from previous fetch), and the exception is preventing latest data from being fetch.
NOTE: I am using com.google.firebase:firebase-firestore:18.1.0
.
TIPS: If you want to change the underlying data type, change the property name as well (e.g. is_active_v2
).
Tip 3: catch toObject exceptions
I think it is advisable to catch all toObject
for potential exception, as underlying data type could be changed accidentally (e.g. Android using Boolean
and Web Client accidentally assign Int
). If you don't catch the exception, a data type changed will cause the App in raise an unrecoverable exception.
Tip 4: runtime data conversion
You can try to write your own runtime custom toObject
function.
The following toObject
function handle changed of is_active
data type from Boolean
to Int
.
class Quote( companion object { const val COLLECTION_NAME = "quote" const val CREATED = "created" const val CONTENT = "content" const val LIKE_COUNT = "like_count" const val IS_ACTIVE = "is_active" fun toObject(snapshot: DocumentSnapshot): Quote? { val isActive = snapshot[IS_ACTIVE] if (isActive is Boolean) { // if isActive is Boolean, convert to Int val data = snapshot.data!! data[IS_ACTIVE] = if (isActive) 1 else 0 // runtime patch // sadly snapshot data cannot be modified directly, so we call an update snapshot.reference.update(mapOf( IS_ACTIVE to data[IS_ACTIVE] )) // https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java return try { CustomClassMapper.convertToCustomClass(data, Quote::class.java) } catch (e: RuntimeException) { Timber.e(e) null } } } }}
NOTE: I don't think this is a good solution, but more of a emergency fix if the app is stuck in unrecoverable exception. While using your own custom toObject
, you can't use the convinient function of QuerySnapshot.
toObjects.
NOTE: Sadly Firestore doesn't support custom adapter for property data type conversion or parsing.