I've been developing a Kotlin back-end service, and stumbled across the Firestore documentSnapshot.toObject(className::class.java)
method.
Take the following Kotlin data class
:
data class Record(
val firstName: String = "",
val lastName: String = "",
val city: String = "",
val country: String = "",
val email: String = "")
And the following code from my Repository
class:
if (documentSnapshot.exists()) {
return documentSnapshot.toObject(Record::class.java)!!
}
Now, from what I understand the method documentSnapshot.toObject(className::class.java)
requires and invokes a no-param default constructor, e.g. val record = Record()
.
This invocation would invoke the primary constructor and assign the default values stated in it (in the case of the data class Record
, the empty strings ""
) to the fields.
Then, it uses public setter methods to set the instance's fields with the values found in the document
.
How this is possible, given that the fields have been marked as val
in the primary data class constructor?
Is reflection at play here?
Is val
not truly final in Kotlin?
Firebase indeed uses reflection to set/get the values. Specifically, it uses JavaBean pattern to identify properties, and then gets/sets them either using their public
getter/setter, or using public
fields.
Your data class
is compiled into the equivalent of this Java code:
public static final class Record {
@NotNull
private final String firstName;
@NotNull
private final String lastName;
@NotNull
private final String city;
@NotNull
private final String country;
@NotNull
private final String email;
@NotNull
public final String getFirstName() { return this.firstName; }
@NotNull
public final String getLastName() { return this.lastName; }
@NotNull
public final String getCity() { return this.city; }
@NotNull
public final String getCountry() { return this.country; }
@NotNull
public final String getEmail() { return this.email; }
public Record(@NotNull String firstName, @NotNull String lastName, @NotNull String city, @NotNull String country, @NotNull String email) {
Intrinsics.checkParameterIsNotNull(firstName, "firstName");
Intrinsics.checkParameterIsNotNull(lastName, "lastName");
Intrinsics.checkParameterIsNotNull(city, "city");
Intrinsics.checkParameterIsNotNull(country, "country");
Intrinsics.checkParameterIsNotNull(email, "email");
super();
this.firstName = firstName;
this.lastName = lastName;
this.city = city;
this.country = country;
this.email = email;
}
...
}
In this case, Firebase uses the public getters to get the property values when i needs to writes them to the database, and the fields when it needs to set the property values when it reads them from the database.