We are working with Firebase a lot in our Flutter apps.
Especially with Firestore.
Passing the map you get from a get()
call to a document or a collection to a mapper and wrapping it in a try catch got old for us really quick and blew up the line count of our API-classes.
To avoid this duplicate code, we wrote three extensions on Firestore classes that are util methods we can call on the snapshots we get.
We are using dart_mappable
to make mapping the raw Firestore data to a class easier.
How we are using the methods
You can use toEntity
on a DocumentSnapshot
and toEntities
on a QuerySnapshot
.toEntities
uses toEntity
internally on all snapshots in the query result.
If toEntity
fails, it will return null
and print the error to the console (this can of course be substituted with proper logging or throwing the error).
Future<UserData?> getUserData({
required String uid,
}) async {
return await _firestore
.collection(FirestoreCollections.users)
.doc(uid)
.get()
.toEntity<UserData>();
}
Future<List<Routine>?> getAllRoutines() async {
return await _firestore
.collection(FirestoreCollections.routines)
.get()
.toEntities<Routine>();
}
The methods in question
Both methods are generic methods that need the type of the result you want passed into them.
If the class you are passing into is not mappable MapperContainer.globals.fromMap
will throw an error and the method will return null.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dart_mappable/dart_mappable.dart';
extension ToEntityExtension on DocumentSnapshot<Object?> {
T? toEntity<T>() {
if (!exists) return null;
try {
return MapperContainer.globals
.fromMap<T>(data()! as Map<String, dynamic>);
} catch (e, s) {
print('Error mapping entity: $e\n$s');
return null;
}
}
}
extension FutureToEntitiesExtension on Future<QuerySnapshot<Object?>> {
Future<List<T>> toEntities<T>() async {
return await then(
(snapshot) => snapshot.docs
.map((doc) => doc.toEntity<T>())
.where((it) => it != null)
.cast<T>()
.toList(),
);
}
}
Mappable Class
dart_mappable
will generate multiple methods for you. In this case, the only one that we are using is the fromMap<T>()
method.
It will take a Map<String, dynamic>
and convert it into an object of the class T
.
Usually you would use the UserDataMapper
and call it fromMap
-method.
Since we wanted a generic method, we found you can use the MapperContainer.globals
-container that holds all registered mappers and get the mapper for the class T
from there.
Because of that, it is important that you register all the mappers you want to use before actually using them.
This can be done with one line.
An example for our UserData
class is this line:
UserDataMapper.ensureInitialized();
Either put it into your main
or in the constructor of your API-classes.
This is what our annotated UserData
class looks like:
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/material.dart';
part 'user_data.mapper.dart';
@MappableClass(caseStyle: CaseStyle.snakeCase)
class UserData with UserDataMappable {
final String id;
final String name;
const UserData({
required this.id,
required this.name,
});
}
TLDR
We are using toEntity
in our Flutter apps to convert Firebase maps to Dart objects. The method uses dart_mappable
in combination with generics and is an extension of the Firebase DocumentSnapshot
class.
Leave a Reply