عرض موقع المستخدم على الخريطة
عرض موقع المستخدم على الخريطة
من أبرز الميزات في أي تطبيق يعتمد على الموقع الجغرافي هي عرض موقع المستخدم الآني على الخريطة مع تحديثه باستمرار أثناء التنقل. في هذا الدرس ستربط بين حزمتين تعرفهما بالفعل — geolocator وgoogle_maps_flutter — لتوسيط الكاميرا على الموقع الحي للمستخدم، وتفعيل طبقة "موقعي" المدمجة، وتحديث علامة مخصصة في الوقت الفعلي عند كل قراءة GPS جديدة.
لماذا لا يكفي استدعاء getCurrentPosition مرة واحدة؟
جلب موقع واحد عند بدء التشغيل مناسب لحالات الاستخدام الأحادية (مثل "ابحث عن المطاعم القريبة")، لكن للحصول على تجربة تتبع حي تحتاج إلى تدفق مستمر. تُرجع Geolocator.getPositionStream() كائن Stream<Position> يُصدر حدثاً جديداً كلما تجاوز الجهاز المسافة المحددة. يحمل كل حدث قيماً حديثة لـ خط العرض وخط الطول والدقة والسرعة والاتجاه.
myLocationEnabled: true في ودجت GoogleMap يعرض طبقة النقطة الزرقاء المدمجة من Google وزر "توسيط الموقع". ومع ذلك تحتاج إلى إدارة StreamSubscription الخاص بك إذا أردت الاستجابة لتغيّرات الموقع في Dart (مثل تحريك الكاميرا، أو تحديث قاعدة بيانات، أو عرض دائرة الدقة).إعداد GoogleMapController
لتحريك الكاميرا برمجياً تحتاج إلى مرجع لـ GoogleMapController الذي يُسلَّم إليك في استدعاء onMapCreated. احفظه داخل Completer حتى يتمكن أي كود لاحق من انتظاره بأمان:
تخزين متحكم الخريطة
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
class LiveLocationMap extends StatefulWidget {
const LiveLocationMap({super.key});
@override
State<LiveLocationMap> createState() => _LiveLocationMapState();
}
class _LiveLocationMapState extends State<LiveLocationMap> {
final Completer<GoogleMapController> _mapController = Completer();
StreamSubscription<Position>? _positionSub;
LatLng _currentLatLng = const LatLng(0, 0);
Set<Marker> _markers = {};
@override
void initState() {
super.initState();
_startTracking();
}
@override
void dispose() {
_positionSub?.cancel();
super.dispose();
}
// ...
}
استخدام Completer مهم لأن onMapCreated يُطلق بشكل غير متزامن بعد عرض الودجت لأول مرة. أي كود يستدعي _mapController.future سينتظر حتى يصبح المتحكم جاهزاً، مما يمنع أخطاء القيمة الفارغة.
الاشتراك في تدفق الموقع
استدعِ Geolocator.getPositionStream() داخل initState (بعد التحقق من الأذونات التي تناولتها في الدرس الخامس). مرر كائن LocationSettings لضبط الدقة وفلتر المسافة. عند كل إصدار حدث، حرّك كاميرا الخريطة وحدّث العلامة داخل setState:
التتبع الحي عبر التدفق
Future<void> _startTracking() async {
// يفترض أن الإذن ممنوح مسبقاً (انظر الدرس الخامس)
const locationSettings = LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10, // أمتار بين التحديثات
);
_positionSub = Geolocator.getPositionStream(
locationSettings: locationSettings,
).listen((Position position) async {
final latLng = LatLng(position.latitude, position.longitude);
// تحريك الكاميرا لمتابعة المستخدم
final controller = await _mapController.future;
await controller.animateCamera(
CameraUpdate.newLatLng(latLng),
);
// تحديث العلامة وإعادة بناء الودجت
setState(() {
_currentLatLng = latLng;
_markers = {
Marker(
markerId: const MarkerId('user_location'),
position: latLng,
infoWindow: InfoWindow(
title: 'أنت هنا',
snippet:
'${position.latitude.toStringAsFixed(5)}, '
'${position.longitude.toStringAsFixed(5)}',
),
),
};
});
});
}
distanceFilter أعلى من صفر (مثل 10 أمتار) لتجنب فيضان الأحداث المتشابهة التي تستنزف البطارية وتؤدي إلى إعادة بناء غير ضرورية. للمشاة تُعدّ 10-15 متراً قيمة جيدة؛ وللمركبات 30-50 متراً.بناء ودجت GoogleMap
مرر متمم المتحكم، وفعّل طبقة الموقع، وأمدّ مجموعة العلامات. تعيين myLocationButtonEnabled: false يُخفي زر إعادة التوسيط الافتراضي من Google لتتمكن من تقديم واجهتك الخاصة:
ودجت GoogleMap مع myLocationEnabled
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('الموقع الحي')),
body: GoogleMap(
onMapCreated: (GoogleMapController controller) {
if (!_mapController.isCompleted) {
_mapController.complete(controller);
}
},
initialCameraPosition: CameraPosition(
target: _currentLatLng,
zoom: 16,
),
myLocationEnabled: true, // النقطة الزرقاء + حلقة الدقة
myLocationButtonEnabled: false, // إخفاء الزر الافتراضي
markers: _markers,
zoomControlsEnabled: true,
),
floatingActionButton: FloatingActionButton(
onPressed: _centreOnUser,
child: const Icon(Icons.my_location),
),
);
}
Future<void> _centreOnUser() async {
final controller = await _mapController.future;
await controller.animateCamera(
CameraUpdate.newLatLngZoom(_currentLatLng, 16),
);
}
التعامل مع الأخطاء وحالات الحافة
- رفض الإذن أثناء التشغيل: لفّ
_startTracking()بـ try/catch لاصطيادPermissionDeniedExceptionوعرض شريط إشعار للمستخدم. - خدمات الموقع معطّلة: اصطد
LocationServiceDisabledExceptionوادفع المستخدم لفتح الإعدادات. - أحداث خطأ في التدفق: مرر معامل
onErrorالاختياري إلى استدعاءlistenلتجنب الاستثناءات غير المعالجة التي تُوقف التدفق بصمت. - الودجت مُزال قبل جاهزية المتحكم: تحقق من
mountedقبل استدعاءsetStateبعد أيawait.
_positionSub?.cancel() داخل dispose(). نسيان إلغاء الاشتراك يُبقي مستشعر GPS نشطاً بعد إزالة الودجت من الشجرة، مما يستنزف بطارية المستخدم وقد يتسبب في خطأ استدعاء setState على ودجت مُزال.ملخص
في هذا الدرس تعلمت كيف تدمج تدفق الموقع من geolocator مع واجهة برمجة الكاميرا في google_maps_flutter لبناء متتبع موقع حي للمستخدم. الخطوات الأساسية هي: الحصول على GoogleMapController عبر Completer، والاشتراك في Geolocator.getPositionStream()، وتحريك الكاميرا عند كل حدث، وتحديث العلامة داخل setState، وتفعيل myLocationEnabled: true للنقطة الزرقاء المدمجة، وإلغاء الاشتراك دائماً في dispose(). بهذه اللبنات يمكنك توسيع الخريطة لعرض دوائر الدقة ومسارات التتبع أو تراكبات الملاحة.