Category: Tech

  • Flutter: Convert your Firstore snapshot to a data-object

    TLDR

    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).

    Dart
    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.

    Dart
    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:

    Dart
    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:

    Dart
    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.