الخرائط والموقع وميزات الجهاز

الحصول على موقع الجهاز باستخدام geolocator

16 دقيقة الدرس 6 من 12

الحصول على موقع الجهاز باستخدام geolocator

حزمة geolocator هي الإضافة الأكثر استخدامًا في Flutter للوصول إلى نظام تحديد المواقع العالمي (GPS) للجهاز ونظام التموضع القائم على الشبكة. توفر واجهة Dart نظيفة ومتعددة المنصات لطلبات الموقع الفردية وتدفقات المواقع المستمرة، وتُعيد كائن Position غنيًا يتضمن خط العرض وخط الطول والارتفاع والدقة والسرعة والاتجاه.

ملاحظة: الموقع إذن حساس. على Android وiOS يجب على المستخدم منح وصول الموقع صراحةً في وقت التشغيل. تتولى حزمة geolocator مطالبات الإذن، لكنك لا تزال مسؤولًا عن الإعلان عن الإدخالات الصحيحة في AndroidManifest.xml وInfo.plist.

إعداد الحزمة

أضف التبعية إلى pubspec.yaml ثم شغّل flutter pub get:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  geolocator: ^13.0.0

لنظام Android، أضف الأذونات التالية داخل وسم <manifest> في ملف android/app/src/main/AndroidManifest.xml:

أذونات AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- فقط إذا احتجت موقعًا في الخلفية -->
<!-- <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> -->

لنظام iOS، أضف المفاتيح التالية إلى ios/Runner/Info.plist:

مفاتيح Info.plist

<key>NSLocationWhenInUseUsageDescription</key>
<string>يستخدم هذا التطبيق موقعك لعرض الأماكن القريبة.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>يحتاج هذا التطبيق إلى موقع الخلفية للتتبع.</string>

التحقق من الأذونات وطلبها

قبل طلب بيانات الموقع يجب التحقق من أن خدمات الموقع مفعّلة على الجهاز وأن تطبيقك يمتلك الإذن اللازم. توفر واجهة geolocator مساعدين مخصصين لكلا الفحصين:

فحص الإذن وطلبه

import 'package:geolocator/geolocator.dart';

Future<void> _checkPermissions() async {
  // 1. هل خدمة الموقع مفعّلة في إعدادات الجهاز؟
  bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    throw Exception('خدمات الموقع معطّلة.');
  }

  // 2. هل منح المستخدم الإذن لهذا التطبيق؟
  LocationPermission permission = await Geolocator.checkPermission();

  if (permission == LocationPermission.denied) {
    // أول مرة أو رفض سابق من المستخدم
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      throw Exception('تم رفض إذن الموقع.');
    }
  }

  if (permission == LocationPermission.deniedForever) {
    // اختار المستخدم "عدم السماح أبدًا" – يجب فتح إعدادات التطبيق يدويًا
    throw Exception('تم رفض إذن الموقع نهائيًا.');
  }
  // الإذن هو whileInUse أو always – يمكن المتابعة بأمان
}

طلب الموقع مرة واحدة

استخدم Geolocator.getCurrentPosition() لجلب لقطة Position واحدة. هذا مثالي للميزات مثل "اعثر على مطاعم قريبة" حيث تحتاج الموقع مرة واحدة فقط. يمكنك ضبط الدقة في مقابل استهلاك البطارية باستخدام تعداد LocationAccuracy:

  • LocationAccuracy.lowest — دقة ~3 كم، أدنى استهلاك للبطارية
  • LocationAccuracy.low — دقة ~1 كم
  • LocationAccuracy.medium — دقة ~100 م
  • LocationAccuracy.high — دقة ~10 م (GPS مفعّل)
  • LocationAccuracy.best — أقصى دقة، أعلى استهلاك للبطارية
  • LocationAccuracy.bestForNavigation — مثل best لكن يستخدم أيضًا أجهزة الاستشعار الحركية

مثال getCurrentPosition

import 'package:geolocator/geolocator.dart';

class LocationService {
  /// تُعيد الموقع الحالي للجهاز بعد التحقق من الأذونات.
  static Future<Position> determinePosition() async {
    bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      return Future.error('خدمات الموقع معطّلة.');
    }

    LocationPermission permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        return Future.error('تم رفض أذونات الموقع');
      }
    }

    if (permission == LocationPermission.deniedForever) {
      return Future.error(
        'تم رفض أذونات الموقع نهائيًا، '
        'لا يمكننا طلب الأذونات.',
      );
    }

    // في هذه النقطة الأذونات ممنوحة ويمكننا
    // الاستمرار في الوصول إلى موقع الجهاز.
    return await Geolocator.getCurrentPosition(
      locationSettings: const LocationSettings(
        accuracy: LocationAccuracy.high,
        timeLimit: Duration(seconds: 15),
      ),
    );
  }
}

// الاستخدام داخل StatefulWidget
Future<void> _fetchLocation() async {
  try {
    final Position position = await LocationService.determinePosition();
    print('خط العرض:  ${position.latitude}');
    print('خط الطول: ${position.longitude}');
    print('الدقة:     ${position.accuracy} م');
    print('الارتفاع:  ${position.altitude} م');
    print('السرعة:    ${position.speed} م/ث');
  } catch (e) {
    print('خطأ: $e');
  }
}
نصيحة: احرص دائمًا على تعيين timeLimit في getCurrentPosition(). بدونه يمكن أن يتعلق الـ Future إلى أجل غير مسمى إذا لم يتمكن GPS من الحصول على إصلاح (مثلًا في الأماكن المغلقة أو الأنفاق). مهلة من 10 إلى 15 ثانية مع تراجع سلس توفر تجربة مستخدم أفضل بكثير.

تدفق الموقع المستمر

استخدم Geolocator.getPositionStream() لحالات الاستخدام التي تتطلب تتبعًا فوريًا مثل التنقل وتطبيقات اللياقة وتتبع التوصيل المباشر. يُعيد Stream<Position> يُصدر قيمًا جديدة كلما تحرك الجهاز أكثر من مسافة الفلتر المُهيأة أو الفاصل الزمني. احرص دائمًا على إلغاء اشتراك التدفق عند التخلص من الودجت لتجنب تسريب الذاكرة:

getPositionStream مع StreamSubscription

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';

class LiveLocationWidget extends StatefulWidget {
  const LiveLocationWidget({super.key});

  @override
  State<LiveLocationWidget> createState() => _LiveLocationWidgetState();
}

class _LiveLocationWidgetState extends State<LiveLocationWidget> {
  StreamSubscription<Position>? _positionSubscription;
  Position? _currentPosition;
  String _statusMessage = 'في انتظار الموقع...';

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

  void _startLocationStream() {
    const locationSettings = LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 10, // يُصدر فقط عند التحرك >= 10 أمتار
    );

    _positionSubscription =
        Geolocator.getPositionStream(locationSettings: locationSettings)
            .listen(
      (Position position) {
        setState(() {
          _currentPosition = position;
          _statusMessage =
              'خط العرض: ${position.latitude.toStringAsFixed(6)}, '
              'خط الطول: ${position.longitude.toStringAsFixed(6)}';
        });
      },
      onError: (Object e) {
        setState(() {
          _statusMessage = 'خطأ في التدفق: $e';
        });
      },
    );
  }

  @override
  void dispose() {
    // ضروري: إلغاء الاشتراك لإيقاف عتاد GPS
    _positionSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(_statusMessage, textAlign: TextAlign.center),
        if (_currentPosition != null) ...[
          Text('الدقة: ${_currentPosition!.accuracy.toStringAsFixed(1)} م'),
          Text('الارتفاع: ${_currentPosition!.altitude.toStringAsFixed(1)} م'),
          Text('السرعة:   ${_currentPosition!.speed.toStringAsFixed(2)} م/ث'),
        ],
      ],
    );
  }
}
تحذير: نسيان استدعاء _positionSubscription?.cancel() في dispose() يُبقي GPS نشطًا بعد إزالة الودجت من الشجرة. هذا يُفرغ البطارية ويمكن أن يسبب استدعاءات setState على ودجت تم التخلص منه مما يؤدي إلى أخطاء في وقت التشغيل.

كائن Position

كل من getCurrentPosition() وgetPositionStream() يُنتجان كائن Position بالخصائص الرئيسية التالية:

  • latitude / longitude — الإحداثيات بالدرجات العشرية (WGS-84)
  • accuracy — الدقة الأفقية المقدّرة بالأمتار
  • altitude — الارتفاع فوق مستوى البحر بالأمتار
  • altitudeAccuracy — الدقة الرأسية المقدّرة بالأمتار (iOS / Android 13+)
  • speed — السرعة الأرضية بالأمتار في الثانية
  • speedAccuracy — دقة السرعة المقدّرة بالأمتار في الثانية
  • heading — اتجاه السير بالدرجات (0 = شمال، في اتجاه عقارب الساعة)
  • timestampDateTime وقت الحصول على الإصلاح

خلاصة

توفر حزمة geolocator كل ما تحتاجه للعمل مع موقع الجهاز في Flutter. استخدم isLocationServiceEnabled() وcheckPermission() / requestPermission() قبل الوصول إلى أي بيانات موقع. استدعِ getCurrentPosition() للحصول على إصلاح فردي مع timeLimit مناسب، واستخدم getPositionStream() مع distanceFilter مناسب للتتبع المباشر. احرص دائمًا على إلغاء StreamSubscription في dispose() لحماية عمر البطارية ومنع أخطاء دورة حياة الودجت.