10

I saw that recently the cloud_firestore: ^2.0.0 update brought a new withConverter functionality.

I want to use it to retrieve and pass my models from and to Firestore, but I am not quite sure how to do this with my existing code.

For example, how do I update the following code to use my custom models?

FirebaseFirestore.instance.collection('movies').add({
  'length': 123,
  'rating': 9.7,
});
creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402

2 Answers2

25

Firestore types

To begin, starting with 2.0.0, all Firestore references & queries are now typed. This means that CollectionReference<T>, DocumentReference<T>, and Query<T> now all have a generic type parameter T.

Default

By default (e.g. when calling FirebaseFirestore.instance.collection()), this generic is Map<String, dynamic>. This means that (without calling withConverter), the data you pass and the data you receive is always of type Map<String, dynamic>.

withConverter

Now, you can make use of withConverter in various places in order to change this type:

final modelRef = FirebaseFirestore.instance
    .collection('models')
    .doc('123')
    .withConverter<Model>(
      fromFirestore: (snapshot, _) => Model.fromJson(snapshot.data()!),
      toFirestore: (model, _) => model.toJson(),
    );
final modelsRef =
    FirebaseFirestore.instance.collection('models').withConverter<Model>(
          fromFirestore: (snapshot, _) => Model.fromJson(snapshot.data()!),
          toFirestore: (model, _) => model.toJson(),
        );
final personsRef = FirebaseFirestore.instance
    .collection('persons')
    .where('age', isGreaterThan: 0)
    .withConverter<Person>(
      fromFirestore: (snapshot, _) => Person.fromJson(snapshot.data()!),
      toFirestore: (model, _) => model.toJson(),
    );

What happens when calling withConverter is that the generic type T is set to your custom Model (e.g. Person in the last example). This means that every subsequent call on the document ref, collection ref, or on the query will all work with that type instead of Map<String, dynamic>.

Usage

The usage of the method is straight-forward:

  • You pass a FromFirestore function that converts a snapshot (with options) to your custom model.
  • You pass a ToFirestore function that converts your model T (with options) back to a Map<String, dynamic>, i.e. Firestore-specific JSON data.

Example

Here is an example of using withConverter with a custom Movie model class (note that I am using freezed because it is more readable):

Future<void> main() async {
  // Create an instance of our model.
  const movie = Movie(length: 123, rating: 9.7);

  // Create an instance of a collection withConverter.
  final collection =
      FirebaseFirestore.instance.collection('movies').withConverter(
            fromFirestore: (snapshot, _) => Movie.fromJson(snapshot.data()!),
            toFirestore: (movie, _) => movie.toJson(),
          );
  
  // Directly add our model to the collection.
  collection.add(movie);
  // Also works for subsequent calls.
  collection.doc('123').set(movie);

  // And also works for reads.
  final Movie movie2 = (await collection.doc('2').get()).data()!;
}

@freezed
class Movie with _$Movie {
  const factory Movie({
    required int length,
    required double rating,
  }) = _Movie;

  factory Movie.fromJson(Map<String, dynamic> json) => _$MovieFromJson(json);
}
creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402
  • 5
    How to get the whole movies collection as a List using this method? – Ahmad Khan Nov 15 '21 at 14:36
  • 2
    Can you use it for Stream>? I can not get my head around how to "turn the data of the snapshot into the model". – Guillem Poy Aug 13 '22 at 05:26
  • Why with DocumentReference you don't need to specify type, but with CollectionReference you have to? I was getting error, but only after saw this answer I noticed that specifying type with CollectionReference fixes the problem. – Konstantin Kozirev Jun 03 '23 at 18:03
3

Let's assume you've a model like this:

class Movie {
  final int length;
  final double rating;

  const Movie({required this.length, required this.rating});

  factory Movie.fromJson(Map<String, dynamic> json) => Movie(
        length: json['length'],
        rating: json['rating'],
      );

  Map<String, Object?> toJson() => {
        'length': length,
        'rating': rating,
      };
}

You can use withConverter like:

void main() async {
  final model = FirebaseFirestore.instance
      .collection('movies')
      .doc()
      .withConverter<Movie>(
    fromFirestore: (snapshot, _) => Movie.fromJson(snapshot.data()!),
    toFirestore: (movie, _) => movie.toJson(),
  );

  final movie = Movie(length: 123, rating: 9.7);
  
  // Write operations
  await model.set(movie);
  await model.delete();
  await model.update(newMovie);

  // Read operation
  final fetchedMovie = (await model.get()).data();
}
iDecode
  • 22,623
  • 19
  • 99
  • 186