Flutter地理位置服务插件geoflutterfire_updated的使用

Flutter地理位置服务插件geoflutterfire_updated的使用

GeoFlutterFire 是一个开源库,允许你基于地理位置存储和查询一组键。其核心功能是存储带字符串键的位置信息。它主要的优势在于能够实时检索给定地理区域内的所有键。

GeoFlutterFire 使用 Firebase Firestore 数据库进行数据存储,允许查询结果随着数据的变化而实时更新。GeoFlutterFire 仅加载某些位置附近的必要数据,使你的应用程序即使在处理超大数据集时也能保持轻量级和响应迅速。

GeoFlutterFire 设计为云_firestore 插件的一个轻量级附加组件。为了保持简单,GeoFlutterFire 在你的 Firestore 数据库中以自己的格式存储数据。这允许你现有的数据格式和安全规则保持不变,同时仍然为你提供易于使用的地理查询解决方案。

开始使用

首先确保在 Flutter 项目中添加 GeoFlutterFire 作为依赖项。

dependencies:
  geoflutterfire_updated: <latest-version>

你也可以直接引用 Git 仓库:

dependencies:
  geoflutterfire_updated:
    git: git://github.com/DarshanGowda0/GeoFlutterFire.git

然后运行 flutter packages get 或在 IntelliJ 中更新包。

初始化

你需要一个带有 Firestore 设置的 Firebase 项目。

import 'package:geoflutterfire_updated/geoflutterfire_updated.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

// 初始化 Firestore 和 GeoFlutterFire
final geo = Geoflutterfire();
final _firestore = FirebaseFirestore.instance;

添加地理数据

将地理数据添加到 Firestore 文档中使用 GeoFirePoint

GeoFirePoint myLocation = geo.point(latitude: 12.960632, longitude: 77.641603);

接下来,使用 Firestore 的 add 方法将 GeoFirePoint 添加到文档中。

_firestore
    .collection('locations')
    .add({'name': 'random name', 'position': myLocation.data});

调用 geoFirePoint.data 返回一个包含 geohash 字符串和 Firestore GeoPoint 的对象。它应该如下所示存储在数据库中。你可以根据需要命名该对象,并且甚至可以在单个文档中保存多个点。

示例数据

查询地理数据

要查询距离某个点 50 公里内的文档集合:

// 创建一个 geoFirePoint
GeoFirePoint center = geo.point(latitude: 12.960632, longitude: 77.641603);

// 获取集合引用或查询
var collectionReference = _firestore.collection('locations');

double radius = 50;
String field = 'position';

Stream<List<DocumentSnapshot>> stream = geo.collection(collectionRef: collectionReference)
                                .within(center: center, radius: radius, field: field);

within 函数返回一个包含 DocumentSnapshot 数据的实时流,还有一些有用的元数据,如与中心点的距离。

stream.listen((List<DocumentSnapshot> documentList) {
      // doSomething()
    });

现在你有一个实时数据流,可以用于在地图上可视化。

示例数据

使用示例

以下是一个完整的示例代码,展示了如何在 Flutter 应用程序中使用 GeoFlutterFire 插件。

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:geoflutterfire_updated/geoflutterfire_updated.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:rxdart/rxdart.dart';

import 'streambuilder_test.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MaterialApp(
    title: 'Geo Flutter Fire example',
    home: MyApp(),
    debugShowCheckedModeBanner: false,
  ));
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  GoogleMapController? _mapController;
  TextEditingController? _latitudeController, _longitudeController;

  // firestore init
  final radius = BehaviorSubject<double>.seeded(1.0);
  final _firestore = FirebaseFirestore.instance;
  final markers = <MarkerId, Marker>{};

  late Stream<List<DocumentSnapshot>> stream;
  late Geoflutterfire geo;

  @override
  void initState() {
    super.initState();
    _latitudeController = TextEditingController();
    _longitudeController = TextEditingController();

    geo = Geoflutterfire();
    GeoFirePoint center = geo.point(latitude: 12.960632, longitude: 77.641603);
    stream = radius.switchMap((rad) {
      final collectionReference = _firestore.collection('locations');

      return geo.collection(collectionRef: collectionReference).within(
          center: center, radius: rad, field: 'position', strictMode: true);

      /*
      ****Example to specify nested object****

      var collectionReference = _firestore.collection('nestedLocations');
//          .where('name', isEqualTo: 'darshan');
      return geo.collection(collectionRef: collectionReference).within(
          center: center, radius: rad, field: 'address.location.position');

      */
    });
  }

  @override
  void dispose() {
    _latitudeController?.dispose();
    _longitudeController?.dispose();
    radius.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('GeoFlutterFire'),
          actions: <Widget>[
            IconButton(
              onPressed: _mapController == null
                  ? null
                  : () {
                      _showHome();
                    },
              icon: Icon(Icons.home),
            )
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Navigator.of(context).push(MaterialPageRoute(builder: (context) {
              return StreamTestWidget();
            }));
          },
          child: Icon(Icons.navigate_next),
        ),
        body: Container(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              Center(
                child: Card(
                  elevation: 4,
                  margin: EdgeInsets.symmetric(vertical: 8),
                  child: SizedBox(
                    width: mediaQuery.size.width - 30,
                    height: mediaQuery.size.height * (1 / 3),
                    child: GoogleMap(
                      onMapCreated: _onMapCreated,
                      initialCameraPosition: const CameraPosition(
                        target: LatLng(12.960632, 77.641603),
                        zoom: 15.0,
                      ),
                      markers: Set<Marker>.of(markers.values),
                    ),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child: Slider(
                  min: 1,
                  max: 200,
                  divisions: 4,
                  value: _value,
                  label: _label,
                  activeColor: Colors.blue,
                  inactiveColor: Colors.blue.withOpacity(0.2),
                  onChanged: (double value) => changed(value),
                ),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Container(
                    width: 100,
                    child: TextField(
                      controller: _latitudeController,
                      keyboardType: TextInputType.number,
                      textInputAction: TextInputAction.next,
                      decoration: InputDecoration(
                          labelText: 'lat',
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          )),
                    ),
                  ),
                  Container(
                    width: 100,
                    child: TextField(
                      controller: _longitudeController,
                      keyboardType: TextInputType.number,
                      decoration: InputDecoration(
                          labelText: 'lng',
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          )),
                    ),
                  ),
                  MaterialButton(
                    color: Colors.blue,
                    onPressed: () {
                      final lat =
                          double.parse(_latitudeController?.text ?? '0.0');
                      final lng =
                          double.parse(_longitudeController?.text ?? '0.0');
                      _addPoint(lat, lng);
                    },
                    child: const Text(
                      'ADD',
                      style: TextStyle(color: Colors.white),
                    ),
                  )
                ],
              ),
              MaterialButton(
                color: Colors.amber,
                child: const Text(
                  'Add nested ',
                  style: TextStyle(color: Colors.white),
                ),
                onPressed: () {
                  final lat = double.parse(_latitudeController?.text ?? '0.0');
                  final lng = double.parse(_longitudeController?.text ?? '0.0');
                  _addNestedPoint(lat, lng);
                },
              )
            ],
          ),
        ),
      ),
    );
  }

  void _onMapCreated(GoogleMapController controller) {
    setState(() {
      _mapController = controller;
//      _showHome();
      // start listening after map is created
      stream.listen((List<DocumentSnapshot> documentList) {
        _updateMarkers(documentList);
      });
    });
  }

  void _showHome() {
    _mapController?.animateCamera(CameraUpdate.newCameraPosition(
      const CameraPosition(
        target: LatLng(12.960632, 77.641603),
        zoom: 15.0,
      ),
    ));
  }

  void _addPoint(double lat, double lng) {
    GeoFirePoint geoFirePoint = geo.point(latitude: lat, longitude: lng);
    _firestore
        .collection('locations')
        .add({'name': 'random name', 'position': geoFirePoint.data}).then((_) {
      print('added ${geoFirePoint.hash} successfully');
    });
  }

  // example to add geoFirePoint inside nested object
  void _addNestedPoint(double lat, double lng) {
    GeoFirePoint geoFirePoint = geo.point(latitude: lat, longitude: lng);
    _firestore.collection('nestedLocations').add({
      'name': 'random name',
      'address': {
        'location': {'position': geoFirePoint.data}
      }
    }).then((_) {
      print('added ${geoFirePoint.hash} successfully');
    });
  }

  void _addMarker(double lat, double lng) {
    final id = MarkerId(lat.toString() + lng.toString());
    final _marker = Marker(
      markerId: id,
      position: LatLng(lat, lng),
      icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueViolet),
      infoWindow: InfoWindow(title: 'latLng', snippet: '$lat,$lng'),
    );
    setState(() {
      markers[id] = _marker;
    });
  }

  void _updateMarkers(List<DocumentSnapshot> documentList) {
    documentList.forEach((DocumentSnapshot document) {
      final data = document.data() as Map<String, dynamic>;
      final GeoPoint point = data['position']['geopoint'];
      _addMarker(point.latitude, point.longitude);
    });
  }

  double _value = 20.0;
  String _label = '';

  changed(value) {
    setState(() {
      _value = value;
      _label = '${_value.toInt().toString()} kms';
      markers.clear();
    });
    radius.add(value);
  }
}

提示

扩展到大规模集合

你可以构建包含数十亿文档的 Firestore 集合。其中一个主要动机是使在查询的数据子集上执行地理查询成为可能。你可以将查询传递给 collection() 方法,那么所有的地理查询将受到该查询约束的限制。

注意:此查询需要一个复合索引,你将在第一次请求时收到 Firestore 错误提示创建。

var queryRef = _firestore.collection('locations').where('city', isEqualTo: 'bangalore');
var stream = geo
              .collection(collectionRef: queryRef)
              .within(center: center, radius: rad, field: 'position');

使用 strictMode

建议对于较小半径使用 strictMode = false,以便利用邻近哈希的文档。

当半径增加到较大数值时,邻近哈希精度会获取到明显远离半径边界范围的文档,因此建议对较大的半径使用 strictMode = true

注意:过滤严格模式发生在客户端,因此在较大半径下过滤是以不必要的文档读取为代价的。

动态查询的 RxDart 方式

var radius = BehaviorSubject<double>.seeded(1.0);
var collectionReference = _firestore.collection('locations');

stream = radius.switchMap((rad) {
      return geo
          .collection(collectionRef: collectionReference)
          .within(center: center, radius: rad, field: 'position');
    });

// 现在更新你的查询
radius.add(25);

更多关于Flutter地理位置服务插件geoflutterfire_updated的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter地理位置服务插件geoflutterfire_updated的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个使用 geoflutterfire_updated 插件在 Flutter 中实现地理位置服务的代码示例。这个插件通常用于结合 Firebase Realtime Database 实现地理位置查询,比如查找附近的用户或地点。

首先,确保你的 Flutter 项目已经添加了 geoflutterfire_updatedcloud_firestore 依赖(如果你使用的是 Firebase Firestore 而不是 Realtime Database)。在 pubspec.yaml 文件中添加以下依赖:

dependencies:
  flutter:
    sdk: flutter
  geoflutterfire_updated: ^3.0.0  # 请检查最新版本号
  cloud_firestore: ^3.1.0  # 请检查最新版本号

然后运行 flutter pub get 来安装这些依赖。

接下来,是一个简单的示例,展示如何使用 geoflutterfire_updated 插件来查询特定范围内的地理位置数据。

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:geoflutterfire_updated/geoflutterfire_updated.dart';
import 'package:geolocator/geolocator.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GeoLocationScreen(),
    );
  }
}

class GeoLocationScreen extends StatefulWidget {
  @override
  _GeoLocationScreenState createState() => _GeoLocationScreenState();
}

class _GeoLocationScreenState extends State<GeoLocationScreen> {
  CollectionReference usersCollection = FirebaseFirestore.instance.collection('users');
  GeoFirePoint? centerGeoPoint;
  double radius = 10.0; // 半径为10公里
  StreamSubscription<List<DocumentSnapshot>>? userStreamSubscription;

  @override
  void initState() {
    super.initState();
    getCurrentLocation();
  }

  void getCurrentLocation() async {
    bool serviceEnabled;
    LocationPermission permission;

    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.');
    }

    Position position = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);
    List<double> coordinates = [position.latitude, position.longitude];

    setState(() {
      centerGeoPoint = geo.point(coordinates: coordinates)!;
    });

    queryNearbyUsers();
  }

  void queryNearbyUsers() {
    if (centerGeoPoint != null) {
      GeoFirePoint center = centerGeoPoint!;
      double radiusInKm = radius;
      String field = 'position';

      GeoFire geoFire = GeoFire(database: usersCollection);
      GeoQuery geoQuery = geoFire.collection(collectionRef: usersCollection)
          .within(center: center, radius: radiusInKm, field: field);

      userStreamSubscription = geoQuery.onSnapshot.listen((snapshot) {
        snapshot.docs.forEach((doc) {
          print('User found within radius: ${doc.data()}');
        });
      });
    }
  }

  @override
  void dispose() {
    userStreamSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GeoLocation Example'),
      ),
      body: Center(
        child: Text(
          centerGeoPoint == null
              ? 'Getting location...'
              : 'Current Location Set, Radius: $radius km',
          style: TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            radius += 1.0;
          });
        },
        tooltip: 'Increase Radius',
        child: Icon(Icons.add),
      ),
    );
  }
}

说明:

  1. 依赖项:确保在 pubspec.yaml 中添加了 geoflutterfire_updatedcloud_firestore
  2. 权限请求:使用 geolocator 插件请求用户的地理位置权限。
  3. 获取当前位置:使用 Geolocator.getCurrentPosition 获取用户的当前位置。
  4. 创建 GeoFire 点:使用 geo.point 方法将获取到的经纬度转换为 GeoFirePoint 对象。
  5. 查询附近用户:使用 GeoFireGeoQuery 查询指定范围内的用户。
  6. 实时监听:使用 onSnapshot 方法监听数据变化。

请注意,这个示例假设你的 Firebase Firestore 中有一个 users 集合,并且每个用户文档中有一个 position 字段,该字段存储了用户的地理位置(经纬度)。

根据你的实际需求,你可能需要调整集合名称、字段名称或查询逻辑。

回到顶部