This is not a straightforward solution, as my use case is slightly differ from the common one.
You will need to add kotlin-reflect
.
depedencies {
implementation "org.jetbrains.kotlin:kotlin-reflect"
}
A lot of the annotation is for Firestore: @IgnoreExtraProperties
, @PropertyName
, which is not necessary for your use case.
@IgnoreExtraPropertiesdata class Card( override var id: String? = null, var modified: Timestamp? = null, var title: String? = null, @get:PropertyName(IMAGE_IDS) @set:PropertyName(IMAGE_IDS) var imageIds: List<String>? = null, // var place: Place? = null,) : FirestoreData { companion object { const val COLLECTION_NAME = "card" const val MODIFIED = "modified" const val TITLE = "title" // const val PLACE = "place" const val IMAGE_IDS = "image_ids" fun colRef(firestore: FirebaseFirestore, uid: String) = UserPrivate.colRef(firestore).document(uid).collection( COLLECTION_NAME ) fun toObject(doc: DocumentSnapshot): Card? { val item = doc.toObject<Card>() item?.id = doc.id return item } } val isNew: Boolean get() = id == null override fun getData(filterNull: Boolean): Map<String, Any?> { return FirestoreDataUtil.toMap(this, excludeNames = listOf(::COLLECTION_NAME.name), filterNull) }}
An Interfect class to indicate support to convert data class to map.
interface FirestoreData { var id: String? // optional fun getData(filterNull: Boolean = true) : Map<String, Any?>}
Process
- Find all the constant public string in
companion object
to find all the field names (ability to exclude certain fields). With this method, I can add new property to this class where I don't want it to be included in the map. - Get the fields and their value based on these field names.
- If object is instance of FirestoreData (like the commented
Place
field), callgetData
to get its underlying field.
class FirestoreDataUtil { companion object { fun toMap(instance: Any, excludeNames: List<String>, filterNull: Boolean = true) : Map<String, Any?> { val findMember = instance::class.declaredMemberProperties .filter{ it.visibility == KVisibility.PUBLIC } .filterIsInstance<KProperty1<Any, *>>() .associateBy( { val annotation = it.getter.findAnnotation<PropertyName>() annotation?.value ?: it.name }, { it } ) var data = instance::class.companionObject!!.declaredMemberProperties .filter { it.visibility == KVisibility.PUBLIC && it.isConst && it.returnType == String::class.createType() && !excludeNames.contains(it.name) } .mapNotNull { it.getter.call(this).toString() } .associateBy( { it }, { // raise exception or return null to ignore val prop = findMember[it] ?: throw Exception("$it not found") val value = prop.get(instance) if (value != null) { /* if (value::class.isData) { value::class.memberFunctions.find { func -> func.name == "toMap" }?.call(value, filterNull) ?: value } */ // TODO("Check value is List<FirestoreData> or Map<Any, FirestoreData>") if (value is FirestoreData) { value.getData(filterNull) } else value } else null } ) if (filterNull) { data = data.filterValues { it != null } } return data } }}
Discussions
I didn't use Json
or Kotlin Serialization
approach because it require each field to be converted into basic type (e.g. Timestamp to epoch milli seconds). I need the value to maintain it's original type.
Originally I use Kotlin Reflect
to find the member fields of the data class, but I might accidentally add a new property (e.g. isNew property) which I don't intend for it to end up in the map.