{"id":20609511,"url":"https://github.com/fluttercommunity/firestore_helpers","last_synced_at":"2025-04-15T04:25:27.478Z","repository":{"id":53766989,"uuid":"139258044","full_name":"fluttercommunity/firestore_helpers","owner":"fluttercommunity","description":"Firestore Helpers - Firestore helper function to create dynamic and location based queries. Maintainer: @escamoteur","archived":false,"fork":false,"pushed_at":"2021-03-20T08:47:28.000Z","size":420,"stargazers_count":49,"open_issues_count":1,"forks_count":19,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-03-28T15:51:52.851Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/firestore_helpers","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fluttercommunity.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-06-30T15:01:56.000Z","updated_at":"2025-03-21T15:46:25.000Z","dependencies_parsed_at":"2022-09-11T15:22:31.585Z","dependency_job_id":null,"html_url":"https://github.com/fluttercommunity/firestore_helpers","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercommunity%2Ffirestore_helpers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercommunity%2Ffirestore_helpers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercommunity%2Ffirestore_helpers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercommunity%2Ffirestore_helpers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fluttercommunity","download_url":"https://codeload.github.com/fluttercommunity/firestore_helpers/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249005175,"owners_count":21197019,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T10:13:46.523Z","updated_at":"2025-04-15T04:25:27.461Z","avatar_url":"https://github.com/fluttercommunity.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Flutter Community: firestore_helpers](https://fluttercommunity.dev/_github/header/firestore_helpers)](https://github.com/fluttercommunity/community)\r\n\r\n\r\n\u003eDISCLAIMER:\r\nAs some users pointed out, FireStore does not correctly compare GeoPoints at this time which lead to more data might be returned from the server than the server-side query intended. As long as you always also use the client-side filtering too by providing a distanceAccessor you will get the right results but if you get a lot of data entries the performance will degrade. Google wants to change that in the Future but no ETA yet. [Please see](https://github.com/firebase/firebase-js-sdk/issues/826#issuecomment-389598387)\r\n\r\n\u003e Warning: V3.0.0 has a breaking change as it fixes a typo clientSite \u003c-\u003e clientSide\r\n\r\n\r\n# FirestoreHelpers\r\n\r\nFireStore is a great database that is easy to work with. To make life even easier here is this package. It contains functions build queries dynamically and for location based queries.\r\n\r\nThe necessary math for the geographical calculations were ported from [this JS source on SO](https://stackoverflow.com/questions/46630507/how-to-run-a-geo-nearby-query-with-firestore) by [Stanton Parham](https://github.com/stparham)\r\n\r\n## Creating Queries dynamically\r\n\r\nIn case you want to modify your queries at runtime `builtQuery()` might be helpful:\r\n\r\n```Dart\r\n/// \r\n/// Builds a query dynamically based on a list of [QueryConstraint] and orders the result based on a list of [OrderConstraint].\r\n/// [collection] : the source collection for the new query\r\n/// [constraints] : a list of constraints that should be applied to the [collection]. \r\n/// [orderBy] : a list of order constraints that should be applied to the [collection] after the filtering by [constraints] was done.\r\n/// Important all limitation of FireStore apply for this method two on how you can query fields in collections and order them.\r\nQuery buildQuery({Query collection, List\u003cQueryConstraint\u003e constraints,\r\n    List\u003cOrderConstraint\u003e orderBy})\r\n\r\n\r\n/// Used by [buildQuery] to define a list of constraints. Important besides the [field] property not more than one of the others can ne [!=null].\r\n/// They corespond to the possisble parameters of Firestore`s [where()] method. \r\nclass QueryConstraint {\r\n  QueryConstraint(\r\n      {this.field,\r\n      this.isEqualTo,\r\n      this.isLessThan,\r\n      this.isLessThanOrEqualTo,\r\n      this.isGreaterThan,\r\n      this.isGreaterThanOrEqualTo,\r\n      this.isNull,\r\n      this.arrayContains});\r\n}\r\n\r\n/// Used by [buildQuery] to define how the results should be ordered. The fields \r\n/// corespond to the possisble parameters of Firestore`s [oderby()] method. \r\nclass OrderConstraint {\r\n  OrderConstraint(this.field, this.descending);\r\n}\r\n```\r\n\r\n\r\n### Example\r\n\r\nLets assume we have an events collection in FireStore and we want to get all events that apply to certain constraints:\r\n\r\n\r\n```Dart\r\n\r\n// Let's assume that our Event class looks like this \r\n\r\nclass Event{\r\n  String id;\r\n  String name;\r\n  DateTime startTime;\r\n  GeoPoint location;\r\n}\r\n\r\n\r\nStream\u003cList\u003cEvent\u003e\u003e getEvents({List\u003cQueryConstraint\u003e constraints}) {\r\n  try {\r\n    Query ref = buildQuery(\r\n      collection: eventCollection, \r\n      constraints: constraints, orderBy: [\r\n          new OrderConstraint(\"startTime\", false),\r\n        ]);\r\n    return ref.snapshots().map((snapShot) =\u003e snapShot.documents.map(eventDoc) {\r\n          var event = _eventSerializer.fromMap(eventDoc.data);\r\n          event.id = eventDoc.documentID;\r\n          return event;\r\n        }).toList());\r\n  } on Exception catch (ex) {\r\n    print(ex);\r\n  }\r\n  return null;\r\n}\r\n\r\n/// And gets called somewhere else\r\n\r\n getEvents(constraints: [new QueryConstraint(field: \"creatorId\", isEqualTo: _currentUser.id)]);\r\n\r\n```\r\n\r\n\r\nTo make this even more comfortable and powerful there is `getDataFromQuery()`\r\n\r\n```Dart\r\ntypedef DocumentMapper\u003cT\u003e = T Function(DocumentSnapshot document);\r\ntypedef ItemFilter\u003cT\u003e = bool Function(T);\r\ntypedef ItemComparer\u003cT\u003e = int Function(T item1, T item2);\r\n\r\n///\r\n/// Convenience Method to access the data of a Query as a stream while applying \r\n/// a mapping function on each document with optional client side filtering and sorting\r\n/// [qery] : the data source\r\n/// [mapper] : mapping function that gets applied to every document in the query. \r\n/// Typically used to deserialize the Map returned from FireStore\r\n/// [clientSideFilters] : optional list of filter functions that execute a `.where()` \r\n/// on the result on the client side\r\n/// [orderComparer] : optional comparisson function. If provided your resulting data \r\n/// will be sorted based on it on the client\r\n\r\nStream\u003cList\u003cT\u003e\u003e getDataFromQuery\u003cT\u003e({\r\n  Query query,\r\n  DocumentMapper\u003cT\u003e mapper,\r\n  List\u003cItemFilter\u003e clientSidefilters,\r\n  ItemComparer\u003cT\u003e orderComparer,\r\n});\r\n```\r\n\r\nWith this our example get this with some additional functionality:\r\n\r\nWe want the events to be ordered by name and only future events. \r\nIn this example we don't do the sorting on the server but on client side after the filtering\r\n\r\n```Dart\r\nStream\u003cList\u003cEvent\u003e\u003e getEvents({List\u003cQueryConstraint\u003e constraints}) {\r\n  try {\r\n    Query query = buildQuery(collection: eventCollection, constraints: constraints);\r\n    return getDataFromQuery(\r\n        query: query, \r\n        mapper: (eventDoc) {\r\n          var event = _eventSerializer.fromMap(eventDoc.data);\r\n          event.id = eventDoc.documentID;\r\n          return event;\r\n        }, \r\n        clientSidefilters: (event) =\u003e event.startTime \u003e DateTime.now()  // only future events\r\n        orderComparer: (event1, event2) =\u003e event1.name.compareTo(event2.name) \r\n      );\r\n  } on Exception catch (ex) {\r\n    print(ex);\r\n  }\r\n  return null;\r\n}\r\n```\r\n\r\n## Location based queries\r\n\r\n\u003eDISCLAIMER:\r\nAs some users pointed out, FireStore does not correctly compare GeoPoints at this time which lead to more data might be returned from the server than the server-side query intended. As long as you always also use the client-side filtering too by providing a distanceAccessor you will get the right results but if you get a lot of data entries the performance will degrade. Google wants to change that in the Future but no ETA yet. [Please see](https://github.com/firebase/firebase-js-sdk/issues/826#issuecomment-389598387)\r\n\r\nA quite common scenario in an mobile App is to query for data that's location entry matches a certain search area.\r\nUnfortunately 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.\r\n\r\nAs most App require to define a search area by a centre point  and a radius we have `calculateBoundingBoxCoordinates`\r\n\r\n```Dart\r\n/// Defines the boundingbox for the query based\r\n/// on its south-west and north-east corners\r\nclass GeoBoundingBox {\r\n  final GeoPoint swCorner;\r\n  final GeoPoint neCorner;\r\n\r\n  GeoBoundingBox({this.swCorner, this.neCorner});\r\n}\r\n\r\n///\r\n/// Defines the search area by a  circle [center] / [radiusInKilometers]\r\n/// Based on the limitations of FireStore we can only search in rectangles\r\n/// which means that from this definition a final search square is calculated\r\n/// that contains the circle\r\nclass Area {\r\n  final GeoPoint center;\r\n  final double radiusInKilometers;\r\n\r\n  Area(this.center, this.radiusInKilometers): \r\n  assert(geoPointValid(center)), assert(radiusInKilometers \u003e= 0);\r\n\r\n  factory Area.inMeters(GeoPoint gp, int radiusInMeters) {\r\n    return new Area(gp, radiusInMeters / 1000.0);\r\n  }\r\n\r\n  factory Area.inMiles(GeoPoint gp, int radiusMiles) {\r\n    return new Area(gp, radiusMiles * 1.60934);\r\n  }\r\n\r\n  /// returns the distance in km of [point] to center\r\n  double distanceToCenter(GeoPoint point) {\r\n    return distanceInKilometers(center, point);\r\n  }\r\n}\r\n\r\n///\r\n///Calculates the SW and NE corners of a bounding box around a center point for a given radius;\r\n/// [area] with the center given as .latitude and .longitude\r\n/// and the radius of the box (in kilometers)\r\nGeoBoundingBox boundingBoxCoordinates(Area area)\r\n```\r\n\r\nIf you use `buildQuery()` is even gets easier with `getLocationsConstraint`\r\n\r\n\r\n```Dart\r\n/// Creates the necessary constraints to query for items in a FireStore collection that are inside a specific range from a center point\r\n/// [fieldName] : the name of the field in FireStore where the location of the items is stored\r\n/// [area] : Area within that the returned items should be\r\nList\u003cQueryConstraint\u003e getLocationsConstraint(String fieldName, Area area) \r\n```\r\n\r\n\r\n```Dart\r\n/// function type used to acces the field that contains the loaction inside \r\n/// the generic type\r\ntypedef LocationAccessor\u003cT\u003e = GeoPoint Function(T item);\r\n\r\n/// function typse used to access the distance field that contains the \r\n/// distance to the target inside the generic type\r\ntypedef DistanceAccessor\u003cT\u003e = double Function(T item);\r\n\r\ntypedef DistanceMapper\u003cT\u003e = T Function(T item, double itemsDistance);\r\n```\r\n\r\n`getDataInArea()` combines all the above functions to one extremely powerful function:\r\n\r\n```Dart\r\n///\r\n/// Provides as Stream of lists of data items of type [T] that have a location field in a\r\n/// specified area sorted by the distance of to the areas center.\r\n/// [area]  : The area that constraints the query\r\n/// [source] : The source FireStore document collection\r\n/// [mapper] : mapping function that gets applied to every document in the query.\r\n/// Typically used to deserialize the Map returned from FireStore\r\n/// [locationFieldInDb] : The name of the data field in your FireStore document.\r\n/// Need to make the location based search on the server side\r\n/// [locationAccessor] : As this is a generic function it cannot know where your\r\n/// location is stored in you generic type.\r\n/// optional if you don't use [distanceMapper] and don't want to sort by distance\r\n/// Therefore pass a function that returns a valur from the location field inside\r\n/// your generic type.\r\n/// [distanceMapper] : optional mapper that gets the distance to the center of the\r\n/// area passed to give you the chance to save this inside your item\r\n/// if you use a [distanceMapper] you HAVE to pass [locationAccessor]\r\n/// [clientSideFilters] : optional list of filter functions that execute a `.where()`\r\n/// on the result on the client side\r\n/// [distanceAccessor] : if you have stored the distance using a [distanceMapper] passing\r\n/// this accessor function will prevent additional distance computing for sorting.\r\n/// [sortDecending] : if the resulting list should be sorted descending by the distance\r\n/// to the area's center. If you don't provide [loacationAccessor] or [distanceAccessor]\r\n/// no sorting is done. This Sorting is done one the client side\r\n/// [serverSideConstraints] : If you need some serverside filtering besides the [Area] pass a list of [QueryConstraint] \r\n/// [serverSideOrdering] : If you need some serverside ordering you can pass a List of [OrderConstraints]  \r\n/// Using [serverSideConstraints] or  [serverSideOrdering] almost always requires to create an index for \r\n/// this field. Check your debug output for a message from FireStore with\r\n/// a link to create them\r\nStream\u003cList\u003cT\u003e\u003e getDataInArea\u003cT\u003e(\r\n    {@required Area area,\r\n    @required Query source,\r\n    @required DocumentMapper\u003cT\u003e mapper,\r\n    @required String locationFieldNameInDB,\r\n    LocationAccessor\u003cT\u003e locationAccessor,\r\n    List\u003cItemFilter\u003cT\u003e\u003e clientSidefilters,\r\n    DistanceMapper\u003cT\u003e distanceMapper,\r\n    DistanceAccessor\u003cT\u003e distanceAccessor,\r\n    bool sortDecending = false,\r\n    List\u003cQueryConstraint\u003e serverSideConstraints,\r\n    List\u003cOrderConstraint\u003e serverSideOrdering})\r\n```\r\n\r\nBest to see an example how we would use it:\r\n\r\n\r\n```Dart\r\n// We will define a wrapper class because we want our Event plut its distance back\r\n\r\nclass EventData{\r\n  Event event;\r\n  double distance;\r\n\r\n  EventData(this.event, [this.distance]) \r\n}\r\n\r\n\r\n\r\nStream\u003cList\u003cEventData\u003e\u003e getEvents(area) {\r\n  try {\r\n    return getDataInArea(\r\n        collection: Firestore.instance.collection(\"events\"),\r\n        area: area,\r\n        locationFieldNameInDB: 'location',        \r\n        mapper: (eventDoc) {\r\n          var event = _eventSerializer.fromMap(eventDoc.data);\r\n          // if you serializer does not pass types like GeoPoint through\r\n          // you have to add that fields manually. If using `jaguar_serializer` \r\n          // add @pass attribute to the GeoPoint field and you can omit this. \r\n          event.location = eventDoc.data['location'] as GeoPoint;\r\n          event.id = eventDoc.documentID;\r\n          return new EventData(event);\r\n        },\r\n        locationAccessor: (eventData) =\u003e eventData.event.location,\r\n        distanceMapper: (eventData, distance) {\r\n          eventData.distance = distance;\r\n          return eventData;\r\n        },\r\n        distanceAccessor: (eventData) =\u003e eventDatas.distance, \r\n        clientSidefilters: (event) =\u003e event.startTime \u003e DateTime.now()  // filer only future events\r\n      );\r\n  } on Exception catch (ex) {\r\n    print(ex);\r\n  }\r\n  return null;\r\n}\r\n```\r\n\r\n\r\n**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.\r\n\r\n```Dart\r\n  Future\u003cbool\u003e updateEvent(Event event) async {\r\n    try {\r\n      var eventData = _eventSerializer.toMap(event);\r\n  -\u003e  eventData['location'] = event.location;\r\n      await eventCollection.document(event.id).setData(eventData);\r\n      return true;\r\n    } catch (e, stack) {\r\n      print(e);\r\n      print(stack.toString());\r\n      //todo logging\r\n      return false;\r\n    }\r\n  }\r\n```\r\n\r\nI use [jaguar_serializer](https://pub.dartlang.org/flutter/packages?q=jaguar_serializer+) which is great in combination with FireStore because it produces a `Map\u003cString, dynamic\u003e` instead of JSON string. To make not to encode GeoPoints but pass them through just add `@pass` attribute to your `GeoPoint` fields.\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluttercommunity%2Ffirestore_helpers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffluttercommunity%2Ffirestore_helpers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluttercommunity%2Ffirestore_helpers/lists"}