firestore_helpers 1.0.0+1 copy "firestore_helpers: ^1.0.0+1" to clipboard
firestore_helpers: ^1.0.0+1 copied to clipboard

outdated

A new flutter package project.

FirestoreHelpers #

FireStore is a great database that is easy to work with. To make live even easier here is this package. It contains functions build queries dynamically and for location based queries.

The necessary math for the geographical calculations were ported from this JS source on SO by Stanton Parham

Creating Queries dynamically #

In case you want to modify your queries at runtime builtQuery() might be helpful:

/// 
/// Builds a query dynamically based on a list of [QueryConstraint] and orders the result based on a list of [OrderConstraint].
/// [collection] : the source collection for the new query
/// [constraints] : a list of constraints that should be applied to the [collection]. 
/// [orderBy] : a list of order constraints that should be applied to the [collection] after the filtering by [constraints] was done.
/// Important all limitation of FireStore apply for this method two on how you can query fields in collections and order them.
Query buildQuery({CollectionReference collection, List<QueryConstraint> constraints,
    List<OrderConstraint> orderBy})


/// Used by [buildQuery] to define a list of constraints. Important besides the [field] property not more than one of the others can ne [!=null].
/// They corespond to the possisble parameters of Firestore`s [where()] method. 
class QueryConstraint {
  QueryConstraint(
      {this.field,
      this.isEqualTo,
      this.isLessThan,
      this.isLessThanOrEqualTo,
      this.isGreaterThan,
      this.isGreaterThanOrEqualTo,
      this.isNull});
}

/// Used by [buildQuery] to define how the results should be ordered. The fields 
/// corespond to the possisble parameters of Firestore`s [oderby()] method. 
class OrderConstraint {
  OrderConstraint(this.field, this.descending);
}

Example #

Lets assume we have an events collection in FireStore and we want to get all events that apply to certain constraints:

Stream<List<Event>> getEvents({List<QueryConstraint> constraints}) {
  try {
    Query ref = buildQuery(
      collection: eventCollection, 
      constraints: constraints, orderBy: [
          new OrderConstraint("location", true),
          new OrderConstraint("startTime", false),
        ]);
    return ref.snapshots().map((snapShot) => snapShot.documents.map(ventDoc) {
          var event = _eventSerializer.fromMap(eventDoc.data);
          // if you serializer does not pass types like GeoPoint through
          // you have to add that fields manually
          event.location = eventDoc.data['location'] as GeoPoint; 
          event.id = eventDoc.documentID;
          return event;
        }).toList());
  } on Exception catch (ex) {
    print(ex);
  }
  return null;
}

/// And gets called somewhere else

 getEvents(constraints: [new QueryConstraint(field: "creatorId", isEqualTo: _currentUser.id)]);

To make this even more comfortable there is getData()

typedef DocumentMapper<T> = T Function(DocumentSnapshot document); 

///
/// Convenience Method to access the data of a Query as a stream while 
/// applying a mapping function on each document
/// [qery] : the data source
/// [mapper] : mapping function that gets applied to every document in the query.
Stream<List<T>> getData<T>(Query query, DocumentMapper<T> mapper)
{
      return query.snapshots().map((snapShot) => snapShot.documents.map(mapper)
          .toList());

}

With this our example get this:

Stream<List<Event>> getEvents({List<QueryConstraint> constraints}) {
  try {
    Query query = buildQuery(collection: eventCollection, constraints: constraints,       
      new OrderConstraint("location", true),
      new OrderConstraint("startTime", false),
    ]);
    return getData(query, (eventDoc) {
          var event = _eventSerializer.fromMap(eventDoc.data);
          // if you serializer does not pass types like GeoPoint through
          // you have to add that fields manually
          event.location = eventDoc.data['location'] as GeoPoint;
          event.id = eventDoc.documentID;
          return event;
        });
  } on Exception catch (ex) {
    print(ex);
  }
  return null;
}

Location based queries #

A quite common scenario in an mobile App is to query for data that's location entry matches a certain search area. Unfortunately FireStore doesn't support real geographical queries, but we can query less than and greater than on GeopPoints. Which allows to span a search square defined by its south-west and north-east corners.

As most App require to define a search area by a center point and a radius we have calculateBoundingBoxCoordinates

/// Defines the boundingbox for the query based
/// on its south-west and north-east corners
class GeoBoundingBox {
  final GeoPoint swCorner;
  final GeoPoint neCorner;

  GeoBoundingBox({this.swCorner, this.neCorner});
}

class Area {
  final GeoPoint center;
  final double radius;

  Area(this.center, this.radius);
}

///
///Calculates the SW and NE corners of a bounding box around a center point for a given radius;
/// [area] with the center given as .latitude and .longitude
/// and the radius of the box (in kilometers)
GeoBoundingBox boundingBoxCoordinates(Area area)

If you use buildQuery() is even gets easier with getLocationsConstraint

/// Creates the necessary constraints to query for items in a FireStore collection that are inside a specific range from a center point
/// [fieldName] : the name of the field in FireStore where the location of the items is stored
/// [area] : Area within that the returned items should be
List<QueryConstraint> getLocationsConstraint(String fieldName, Area area) 

IMPORTANT to enable FireStore to execute queries based on GeopPoints you can not serialize the GeoPoints before you hand them to FireStore's setData if you use a code generator that does not allow to mark certain field as passthrough you have to set the value manually like here.

  Future<bool> updateEvent(Event event) async {
    try {
      var eventData = _eventSerializer.toMap(event);
      eventData['location'] = event.location;
      await eventCollection.document(event.id).setData(eventData);
      return true;
    } catch (e, stack) {
      print(e);
      print(stack.toString());
      //todo logging
      return false;
    }
  }

I use jaguar_serializer which is great in combination with FireStore because it produces a Map<String, dynamic> instead of JSON string. Currently jaguar has a problem when with GeoPoints (which will be fixed soon hopefully) so best to mark location field with @Field(dontDecode: true, dontEncode: true) and add the value manually as seen above