Flutter

Using Flutter geolocation to perform geoqueries on Parse

Introduction

In this guide, you will learn about GeoPoint data type on Parse and how to perform geo queries in Parse using the Flutter geolocation. You will create an application that will perform geo queries and retrieve records using Parse Geopoints.

We won’t explain the Flutter application code once this guide’s primary focus is on the Flutter with Parse.

Prerequisites

To complete this tutorial, you will need:

  • Flutter version 2.2.x or later
  • Android Studio or VS Code installed (with Plugins Dart and Flutter)
  • An app created on Back4App.
  • An Flutter app connected to Back4app.
  • A device (or virtual device) running Android or iOS.
  • In order to run this guide example you should set up the plugin Geolocator properly. Do not forget to add permissions for Android and iOS in order to access the device location {: .btn target=”_blank” rel=”nofollow noreferer noopener”} in the project. Carefully read the instructions for setting up the Android and iOS project.

Goal

Perform Geoqueries using geopoints stored on Back4App and Flutter geolocation.

The GeoPoint datatype

Parse allows you to associate real-world latitude and longitude coordinates with an object. Adding a ParseGeoPoint to a ParseObject will enable queries to consider the proximity of an object to a reference point. This feature allows you to easily do things like finding out what user is closest to another user or which places are nearest to a user.

To associate a point with an object, you first need to create a ParseGeoPoint. Below you can find a geopoint with a latitude of 40.0 degrees and -30.0 degrees longitude.

1
final point = ParseGeoPoint(latitude: 40.0, longitude: 30.0);

This point is then stored in the object as a regular field, like any other data type (string, number, date, etc.)

1
2
placeObject.set("location", point);
await placeObject.save();

** Note: Currently only one key in a class may be a ParseGeoPoint

Step 1 - The QueryBuilder class

Any Parse query operation uses the QueryBuilder object type, which will help you retrieve specific data from your database throughout your app. To create a new QueryBuilder, you need to pass as a parameter the desired ParseObject subclass, which is the one that will contain your query results.

It is crucial to know that a QueryBuilder will only resolve after calling a retrieve method query, so a query can be set up and several modifiers can be chained before actually being called. You can read more about the QueryBuilder class here at the official documentation.

Using the JavaScript Console on Back4App

Inside your Back4App application’s dashboard, you will find a very useful API console in which you can run JavaScript code directly. In this guide you will use to store data objects in Back4App. On your App main dashboard go to Core->API Console->Javascript.

Flutter Back4App

Step 2 - Save Data on Back4app

To run the queries on this guide you’ll need first to populate your App with some data. Let’s create a City class, which will be the target of our queries in this guide. Here is the Parse.Object classes creation code, so go ahead and run it in your API console:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Add City objects and create table
// Note how GeoPoints are created, passing latitude and longitude as arguments
// Montevideo
City = new Parse.Object('City');
City.set('name', 'Montevideo - Uruguay');
City.set('location', new Parse.GeoPoint(-34.85553195363169, -56.207280375137955));
await City.save();

// Brasília
City = new Parse.Object('City');
City.set('name', 'Brasília - Brazil');
City.set('location', new Parse.GeoPoint(-15.79485821477289, -47.88391074690196));
await City.save();

// Bogotá
City = new Parse.Object('City');
City.set('name', 'Bogotá - Colombia');
City.set('location', new Parse.GeoPoint(4.69139880891712, -74.06936691331047));
await City.save();

// Mexico City
City = new Parse.Object('City');
City.set('name', 'Mexico City - Mexico');
City.set('location', new Parse.GeoPoint(19.400977162618933, -99.13311378164776));
await City.save();

// Washington, D.C.
City = new Parse.Object('City');
City.set('name', 'Washington, D.C. - USA');
City.set('location', new Parse.GeoPoint(38.930727220189944, -77.04626261880388));
await City.save();

// Ottawa
City = new Parse.Object('City');
City.set('name', 'Ottawa - Canada');
City.set('location', new Parse.GeoPoint(45.41102167733425, -75.695414598736));
await City.save();

// Tokyo
City = new Parse.Object('City');
City.set('name', 'Tokyo - Japan');
City.set('location', new Parse.GeoPoint(35.6897, 139.6922));
await City.save();

// Mumbai
City = new Parse.Object('City');
City.set('name', 'Mumbai - India');
City.set('location', new Parse.GeoPoint(18.9667, 72.8333));
await City.save();

// Shanghai
City = new Parse.Object('City');
City.set('name', 'Shanghai - China');
City.set('location', new Parse.GeoPoint(31.1667, 121.4667));
await City.save();

// New York
City = new Parse.Object('City');
City.set('name', 'New York - USA');
City.set('location', new Parse.GeoPoint(40.6943, -73.9249));
await City.save();

// Moscow
City = new Parse.Object('City');
City.set('name', 'Moscow - Russia');
City.set('location', new Parse.GeoPoint(55.7558, 37.6178));
await City.save();

// Paris
City = new Parse.Object('City');
City.set('name', 'Paris - France');
City.set('location', new Parse.GeoPoint(48.8566, 2.3522));
await City.save();

// Paris
City = new Parse.Object('City');
City.set('name', 'London - United Kingdom');
City.set('location', new Parse.GeoPoint(51.5072, -0.1275));
await City.save();

// Luanda
City = new Parse.Object('City');
City.set('name', 'Luanda - Angola');
City.set('location', new Parse.GeoPoint(-8.8383, 13.2344));
await City.save();

// Johannesburg
City = new Parse.Object('City');
City.set('name', 'Johannesburg - South Africa');
City.set('location', new Parse.GeoPoint(-26.2044, 28.0416));
await City.save();

console.log('Success!');

After running this code, you should now have a City class in your database. Your new class should look like this:

Fluuter Back4App

Let’s now take a look at examples from every QueryBuilder method, along with brief explanations on what they do.

Step 3 - Query the data

Now that you have a populated class, we can now perform some GeoPoint queries in it.

Let’s begin by ordering City results by the nearest from Dallas in USA (latitude 32.779167, and longitude -96.808891), using the whereNear method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // Create your query
    final QueryBuilder<ParseObject> parseQuery =
        QueryBuilder<ParseObject>(ParseObject('City'));

    // Create our GeoPoint for the query
    final dallasGeoPoint =
        ParseGeoPoint(latitude: 32.779167, longitude: -96.808891);

    // `whereNear` will order results based on distance between the GeoPoint
    // type field from the class and the GeoPoint argument
    parseQuery.whereNear('location', dallasGeoPoint);

    // The query will resolve only after calling this method, retrieving
    // an array of `ParseObjects`, if success
    final ParseResponse apiResponse = await parseQuery.query();

    if (apiResponse.success && apiResponse.results != null) {
      // Let's show the results
      for (var o in apiResponse.results! as List<ParseObject>) {
        print(
            'City: ${o.get<String>('name')} - Location: ${o.get<ParseGeoPoint>('location')!.latitude}, ${o.get<ParseGeoPoint>('location')!.longitude}');
      }
    }

Let’s now query using the method whereWithinKilometers, which will retrieve all results whose GeoPoint field is located within the max distance in Kilometers.
Dallas will be used once again as a reference and the distance limit will be 3000 km.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    // Create your query
    final QueryBuilder<ParseObject> parseQuery =
        QueryBuilder<ParseObject>(ParseObject('City'));

    // Create our GeoPoint for the query
    final dallasGeoPoint =
        ParseGeoPoint(latitude: 32.779167, longitude: -96.808891);

    // You can also use `withinMiles` and `withinRadians` the same way,
    // but with different measuring unities
    parseQuery.whereWithinKilometers('location', dallasGeoPoint, 3000);
    //parseQuery.whereWithinMiles('location', dallasGeoPoint, 3000);

    // The query will resolve only after calling this method, retrieving
    // an array of `ParseObjects`, if success
    final ParseResponse apiResponse = await parseQuery.query();

    if (apiResponse.success && apiResponse.results != null) {
      // Let's show the results
      for (var o in apiResponse.results! as List<ParseObject>) {
        print(
            'City: ${o.get<String>('name')} - Location: ${o.get<ParseGeoPoint>('location')!.latitude}, ${o.get<ParseGeoPoint>('location')!.longitude}');
      }
    }

Let’s now query using the method whereWithinMiles, which will retrieve all results whose GeoPoint field is located within the max distance in Miles.
Dallas will be used once again as a reference and the distance limit will be 3000 miles.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // Create your query
    final QueryBuilder<ParseObject> parseQuery =
        QueryBuilder<ParseObject>(ParseObject('City'));

    // Create our GeoPoint for the query
    final dallasGeoPoint =
        ParseGeoPoint(latitude: 32.779167, longitude: -96.808891);

    // You can also use `whereWithinKilometers` and `whereWithinRadians` the same way,
    parseQuery.whereWithinMiles('location', dallasGeoPoint, 3000);

    // The query will resolve only after calling this method, retrieving
    // an array of `ParseObjects`, if success
    final ParseResponse apiResponse = await parseQuery.query();

    if (apiResponse.success && apiResponse.results != null) {
      // Let's show the results
      for (var o in apiResponse.results! as List<ParseObject>) {
        print(
            'City: ${o.get<String>('name')} - Location: ${o.get<ParseGeoPoint>('location')!.latitude}, ${o.get<ParseGeoPoint>('location')!.longitude}');
      }
    }

Step 4 - Query from Flutter

Let’s now use our example queries inside a Flutter App, with a simple interface having a list showing results and also 3 buttons for calling the queries.

The app also retrieves the device’s current location using Geolocator plugin (follow the instructions), so the queries will be using real data.

Open your Flutter project, go to the main.dart file, clean up all the code, and replace it with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final keyApplicationId = 'YOUR_APP_ID_HERE';
  final keyClientKey = 'YOUR_CLIENT_KEY_HERE';

  final keyParseServerUrl = 'https://parseapi.back4app.com';

  await Parse().initialize(keyApplicationId, keyParseServerUrl,
      clientKey: keyClientKey, debug: true);

  runApp(MaterialApp(
    title: 'Flutter - GeoPoint',
    debugShowCheckedModeBanner: false,
    home: HomePage(),
  ));
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<ParseObject> results = <ParseObject>[];
  double selectedDistance = 3000;

  Future<Position> getCurrentPosition() async {
    bool serviceEnabled;
    LocationPermission permission;

    // Test if location services are enabled.
    serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      return Future.error('Location services are disabled.');
    }

    permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        return Future.error('Location permissions are denied');
      }
    }

    if (permission == LocationPermission.deniedForever) {
      return Future.error(
          'Location permissions are permanently denied, we cannot request permissions.');
    }

    // When we reach here, permissions are granted and we can
    // continue accessing the position of the device.
    return await Geolocator.getCurrentPosition();
  }

  void doQueryNear() async {
    // Create your query
    final QueryBuilder<ParseObject> parseQuery =
        QueryBuilder<ParseObject>(ParseObject('City'));

    // Get current position from device
    final position = await getCurrentPosition();

    final currentGeoPoint = ParseGeoPoint(
        latitude: position.latitude, longitude: position.longitude);

    // `whereNear` will order results based on distance between the GeoPoint
    // type field from the class and the GeoPoint argument
    parseQuery.whereNear('location', currentGeoPoint);

    // The query will resolve only after calling this method, retrieving
    // an array of `ParseObjects`, if success
    final ParseResponse apiResponse = await parseQuery.query();

    if (apiResponse.success && apiResponse.results != null) {
      // Let's show the results
      for (var o in apiResponse.results! as List<ParseObject>) {
        print(
            'City: ${o.get<String>('name')} - Location: ${o.get<ParseGeoPoint>('location')!.latitude}, ${o.get<ParseGeoPoint>('location')!.longitude}');
      }

      setState(() {
        results = apiResponse.results as List<ParseObject>;
      });
    } else {
      setState(() {
        results.clear();
      });
    }
  }

  void doQueryInKilometers() async {
    // Create your query
    final QueryBuilder<ParseObject> parseQuery =
        QueryBuilder<ParseObject>(ParseObject('City'));

    // Get current position from device
    final position = await getCurrentPosition();

    final currentGeoPoint = ParseGeoPoint(
        latitude: position.latitude, longitude: position.longitude);

    // You can also use `whereWithinMiles` and `whereWithinRadians` the same way,
    // but with different measuring unities
    parseQuery.whereWithinKilometers(
        'location', currentGeoPoint, selectedDistance);

    // The query will resolve only after calling this method, retrieving
    // an array of `ParseObjects`, if success
    final ParseResponse apiResponse = await parseQuery.query();

    if (apiResponse.success && apiResponse.results != null) {
      // Let's show the results
      for (var o in apiResponse.results! as List<ParseObject>) {
        print(
            'City: ${o.get<String>('name')} - Location: ${o.get<ParseGeoPoint>('location')!.latitude}, ${o.get<ParseGeoPoint>('location')!.longitude}');
      }

      setState(() {
        results = apiResponse.results as List<ParseObject>;
      });
    } else {
      setState(() {
        results.clear();
      });
    }
  }

  void doQueryInMiles() async {
    // Create your query
    final QueryBuilder<ParseObject> parseQuery =
        QueryBuilder<ParseObject>(ParseObject('City'));

    // Get current position from device
    final position = await getCurrentPosition();

    final currentGeoPoint = ParseGeoPoint(
        latitude: position.latitude, longitude: position.longitude);

    // You can also use `whereWithinKilometers` and `whereWithinRadians` the same way,
    parseQuery.whereWithinMiles('location', currentGeoPoint, selectedDistance);

    // The query will resolve only after calling this method, retrieving
    // an array of `ParseObjects`, if success
    final ParseResponse apiResponse = await parseQuery.query();

    if (apiResponse.success && apiResponse.results != null) {
      // Let's show the results
      for (var o in apiResponse.results! as List<ParseObject>) {
        print(
            'City: ${o.get<String>('name')} - Location: ${o.get<ParseGeoPoint>('location')!.latitude}, ${o.get<ParseGeoPoint>('location')!.longitude}');
      }

      setState(() {
        results = apiResponse.results as List<ParseObject>;
      });
    } else {
      setState(() {
        results.clear();
      });
    }
  }

  void doClearResults() async {
    setState(() {
      results = [];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Container(
            height: 200,
            child: Image.network(
                'https://blog.back4app.com/wp-content/uploads/2017/11/logo-b4a-1-768x175-1.png'),
          ),
          Center(
            child: const Text('Flutter on Back4app - GeoPoint',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          ),
          SizedBox(
            height: 8,
          ),
          Container(
            height: 50,
            child: ElevatedButton(
                onPressed: doQueryNear,
                child: Text('Query Near'),
                style: ElevatedButton.styleFrom(primary: Colors.blue)),
          ),
          SizedBox(
            height: 16,
          ),
          Center(child: Text('Distance')),
          Slider(
            value: selectedDistance,
            min: 0,
            max: 10000,
            divisions: 10,
            onChanged: (value) {
              setState(() {
                selectedDistance = value;
              });
            },
            label: selectedDistance.toStringAsFixed(0),
          ),
          SizedBox(
            height: 8,
          ),
          Container(
            height: 50,
            child: ElevatedButton(
                onPressed: doQueryInKilometers,
                child: Text('Query in Kilometers'),
                style: ElevatedButton.styleFrom(primary: Colors.blue)),
          ),
          SizedBox(
            height: 8,
          ),
          Container(
            height: 50,
            child: ElevatedButton(
                onPressed: doQueryInMiles,
                child: Text('Query Miles'),
                style: ElevatedButton.styleFrom(primary: Colors.blue)),
          ),
          SizedBox(
            height: 8,
          ),
          Container(
            height: 50,
            child: ElevatedButton(
                onPressed: doClearResults,
                child: Text('Clear results'),
                style: ElevatedButton.styleFrom(primary: Colors.blue)),
          ),
          SizedBox(
            height: 8,
          ),
          Text(
            'Result List: ${results.length}',
          ),
          Expanded(
            child: ListView.builder(
                itemCount: results.length,
                itemBuilder: (context, index) {
                  final o = results[index];
                  return Container(
                    padding: const EdgeInsets.all(4),
                    decoration:
                        BoxDecoration(border: Border.all(color: Colors.black)),
                    child: Text(
                        '${o.get<String>('name')} \nLocation: ${o.get<ParseGeoPoint>('location')!.latitude}, ${o.get<ParseGeoPoint>('location')!.longitude}'),
                  );
                }),
          )
        ],
      ),
    ));
  }
}

Find your Application Id and Client Key credentials navigating to your app Dashboard at Back4App Website.

Update your code in main.dart with the values of your project’s ApplicationId and ClientKey in Back4app.

  • keyApplicationId = App Id
  • keyClientKey = Client Key

Run the project, and the app will load as shown in the image.

flutter-back4app-associations

Conclusion

At the end of this guide, you learned how GeoPoint data queries work on Parse and how to perform them on Back4App from a Flutter App.