1

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?

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • 2
    [Nothing is safe with reflection](https://stackoverflow.com/a/4516608/6296561). – Zoe May 12 '19 at 12:17

1 Answers1

1

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.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • 2
    To be a little more specific, the fact that each val was given a default value lets Kotlin create a no-arg constructor, which allows the Firebase SDK to instantiate the data class in the first place, without having to know all the values that it could hold. So you will end up with `public Record() {...}` as well as the primary constructor, and all of the accessors. – Doug Stevenson May 12 '19 at 17:16
  • 1
    Thanks for clarifying that Doug. I was wondering why this got a default constructor, where other `data class` examples did not. – Frank van Puffelen May 12 '19 at 20:43