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:

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 centre 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});
}
///
/// Defines the search area by a circle [center] / [radiusInKilometers]
/// Based on the limitations of FireStore we can only search in rectangles
/// which means that from this definition a final search square is calculated
/// that contains the circle
class Area {
final GeoPoint center;
final double radiusInKilometers;
Area(this.center, this.radiusInKilometers):
assert(geoPointValid(center)), assert(radiusInKilometers >= 0);
factory Area.inMeters(GeoPoint gp, int radiusInMeters) {
return new Area(gp, radiusInMeters / 1000.0);
}
factory Area.inMiles(GeoPoint gp, int radiusMiles) {
return new Area(gp, radiusMiles * 1.60934);
}
/// returns the distance in km of [point] to center
double distanceToCenter(GeoPoint point) {
return distanceInKilometers(center, point);
}
}
///
///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)

/// function type used to acces the field that contains the loaction inside
/// the generic type
typedef LocationAccessor<T> = GeoPoint Function(T item);
/// function typse used to access the distance field that contains the
/// distance to the target inside the generic type
typedef DistanceAccessor<T> = double Function(T item);
typedef DistanceMapper<T> = T Function(T item, double itemsDistance);
`getDataInArea()` combines all the above functions to one extremely powerful function:
///
/// Provides as Stream of lists of data items of type [T] that have a location field in a
/// specified area sorted by the distance of to the areas center.
/// [area] : The area that constraints the query
/// [collection] : The source FireStore document collection
/// [mapper] : mapping function that gets applied to every document in the query.
/// Typically used to deserialize the Map returned from FireStore
/// [locationFieldInDb] : The name of the data field in your FireStore document.
/// Need to make the location based search on the server side
/// [locationAccessor] : As this is a generic function it cannot know where your
/// location is stored in you generic type.
/// optional if you don't use [distanceMapper] and don't want to sort by distance
/// Therefore pass a function that returns a valur from the location field inside
/// your generic type.
/// [distanceMapper] : optional mapper that gets the distance to the center of the
/// area passed to give you the chance to save this inside your item
/// if you use a [distanceMapper] you HAVE to pass [locationAccessor]
/// [clientSideFilters] : optional list of filter functions that execute a `.where()`
/// on the result on the client side
/// [distanceAccessor] : if you have stored the distance using a [distanceMapper] passing
/// this accessor function will prevent additional distance computing for sorting.
/// [sortDecending] : if the resulting list should be sorted descending by the distance
/// to the area's center. If you don't provide [loacationAccessor] or [distanceAccessor]
/// no sorting is done
Stream<List<T>> getDataInArea<T>(
{@required Area area,
@required CollectionReference collection,
@required DocumentMapper<T> mapper,
@required String locationFieldNameInDB,
LocationAccessor<T> locationAccessor,
List<ItemFilter> clientSitefilters,
DistanceMapper<T> distanceMapper,
DistanceAccessor<T> distanceAccessor,
bool sortDecending = false}) {
assert((distanceAccessor == null) || (distanceMapper != null && distanceAccessor != null),);

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. If using jaguar_serializer add @pass attribute to the GeoPoint field and you can omit this.

I use jaguar_serializer which is great in combination with FireStore because it produces a Map<String, dynamic> instead of JSON string. To make not to encode GeoPoints but pass them through just add @pass attribute to your GeoPoint fields.