Flutter'da Harita Entegrasyonu: Google Maps ve flutter_map

Harita entegrasyonu için iki ana seçenek vardır: google_maps_flutter (Google Maps API, ücretli ama kapsamlı) ve flutter_map (OpenStreetMap tabanlı, tamamen açık kaynak ve ücretsiz). Her ikisini de örneklerle inceleyeceğiz.

google_maps_flutter: Kurulum ve Temel Kullanım

// pubspec:
// google_maps_flutter: ^2.9.0
// geolocator: ^13.0.2
// geocoding: ^3.0.0

// Android: google_services.json ve API key
// android/app/src/main/AndroidManifest.xml
// <meta-data android:name="com.google.android.geo.API_KEY"
//            android:value="YOUR_API_KEY"/>

// iOS: AppDelegate.swift
// GMSServices.provideAPIKey("YOUR_API_KEY")

class MapPage extends StatefulWidget {
  const MapPage({super.key});
  @override State<MapPage> createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
  GoogleMapController? _mapController;
  final Set<Marker> _markers = {};
  final Set<Polyline> _polylines = {};
  LatLng? _userLocation;

  static const _istanbul = LatLng(41.0082, 28.9784);

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

  Future<void> _loadUserLocation() async {
    final permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) return;

    final pos = await Geolocator.getCurrentPosition(
      locationSettings: const LocationSettings(
        accuracy: LocationAccuracy.high,
        distanceFilter: 10,
      ),
    );

    final latLng = LatLng(pos.latitude, pos.longitude);
    setState(() {
      _userLocation = latLng;
      _markers.add(Marker(
        markerId: const MarkerId('user'),
        position: latLng,
        icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
        infoWindow: const InfoWindow(title: 'Konumunuz'),
      ));
    });
    _mapController?.animateCamera(CameraUpdate.newLatLngZoom(latLng, 15));
  }

  void _addBusinessMarker(Business business) {
    _markers.add(Marker(
      markerId: MarkerId(business.id),
      position: LatLng(business.lat, business.lng),
      icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueOrange),
      infoWindow: InfoWindow(
        title: business.name,
        snippet: business.address,
        onTap: () => _showBusinessSheet(business),
      ),
    ));
    setState(() {});
  }

  void _drawRoute(List<LatLng> points) {
    setState(() {
      _polylines.add(Polyline(
        polylineId: const PolylineId('route'),
        points: points,
        color: Colors.blue,
        width: 5,
        patterns: [PatternItem.dash(30), PatternItem.gap(10)],
      ));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GoogleMap(
        initialCameraPosition: const CameraPosition(
          target: _istanbul,
          zoom: 12,
        ),
        markers: _markers,
        polylines: _polylines,
        myLocationEnabled: true,
        myLocationButtonEnabled: true,
        mapType: MapType.normal,
        trafficEnabled: false,
        onMapCreated: (controller) {
          _mapController = controller;
          // Özel harita stili (JSON)
          controller.setMapStyle(_darkMapStyle);
        },
        onTap: (latLng) {
          debugPrint('Tıklanan: ${latLng.latitude}, ${latLng.longitude}');
        },
        onLongPress: (latLng) => _addCustomMarker(latLng),
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton.small(
            onPressed: _loadUserLocation,
            child: const Icon(Icons.my_location),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            onPressed: () => _mapController?.animateCamera(
              CameraUpdate.newLatLngZoom(_istanbul, 12)),
            child: const Icon(Icons.map),
          ),
        ],
      ),
    );
  }

  void _addCustomMarker(LatLng position) async {
    // Özel marker görseli
    final icon = await BitmapDescriptor.asset(
      const ImageConfiguration(size: Size(48, 48)),
      'assets/icons/pin.png',
    );
    setState(() {
      _markers.add(Marker(
        markerId: MarkerId(position.toString()),
        position: position,
        icon: icon,
      ));
    });
  }

  static const _darkMapStyle = '''[
    {"elementType": "geometry", "stylers": [{"color": "#212121"}]},
    {"elementType": "labels.text.fill", "stylers": [{"color": "#757575"}]}
  ]''';
}

flutter_map: Ücretsiz OpenStreetMap Alternatifi

// pubspec:
// flutter_map: ^7.0.2
// latlong2: ^0.9.1

class OpenStreetMapPage extends StatefulWidget {
  const OpenStreetMapPage({super.key});
  @override State<OpenStreetMapPage> createState() => _OSMPageState();
}

class _OSMPageState extends State<OpenStreetMapPage> {
  final _mapController = MapController();
  final List<Marker> _markers = [];
  final List<Polyline> _routes = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FlutterMap(
        mapController: _mapController,
        options: const MapOptions(
          initialCenter: LatLng(41.0082, 28.9784),
          initialZoom: 13,
          minZoom: 3,
          maxZoom: 18,
        ),
        children: [
          // Temel harita katmanı
          TileLayer(
            urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            userAgentPackageName: 'com.example.app',
            maxZoom: 19,
            // Offline tile caching için flutter_map_tile_caching
          ),
          // Rotalar
          PolylineLayer(
            polylines: _routes,
          ),
          // Marker'lar
          MarkerLayer(markers: _markers),
          // Kullanıcı konumu halkası
          CircleLayer(
            circles: [
              if (_userPos != null)
                CircleMarker(
                  point: _userPos!,
                  radius: 80,
                  useRadiusInMeter: true,
                  color: Colors.blue.withOpacity(0.2),
                  borderColor: Colors.blue,
                  borderStrokeWidth: 2,
                ),
            ],
          ),
          // Ölçek çubuğu
          const RichAttributionWidget(
            attributions: [
              TextSourceAttribution('OpenStreetMap contributors'),
            ],
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _mapController.move(LatLng(41.0082, 28.9784), 13),
        child: const Icon(Icons.center_focus_strong),
      ),
    );
  }

  LatLng? _userPos;

  void addRoute(List<LatLng> points, {Color color = Colors.blue}) {
    setState(() {
      _routes.add(Polyline(
        points: points,
        strokeWidth: 4,
        color: color,
      ));
    });
  }

  void addMarker(LatLng pos, {required Widget child}) {
    setState(() {
      _markers.add(Marker(
        point: pos,
        width: 48,
        height: 48,
        child: child,
      ));
    });
  }
}

Konum Takibi: Anlık ve Sürekli

@riverpod
class LocationNotifier extends _$LocationNotifier {
  StreamSubscription<Position>? _sub;

  @override
  LocationState build() {
    ref.onDispose(() => _sub?.cancel());
    return const LocationState();
  }

  Future<void> startTracking() async {
    final permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      final result = await Geolocator.requestPermission();
      if (result == LocationPermission.denied) {
        state = state.copyWith(error: 'Konum izni reddedildi');
        return;
      }
    }

    _sub = Geolocator.getPositionStream(
      locationSettings: const LocationSettings(
        accuracy: LocationAccuracy.high,
        distanceFilter: 5, // en az 5m değişince bildir
      ),
    ).listen((position) {
      final latLng = LatLng(position.latitude, position.longitude);
      state = state.copyWith(
        currentPosition: latLng,
        path: [...state.path, latLng],
        speed: position.speed, // m/s
        heading: position.heading,
      );
    });
  }

  void stopTracking() {
    _sub?.cancel();
    _sub = null;
  }

  // İki nokta arası mesafe (metre)
  double distanceTo(LatLng target) {
    if (state.currentPosition == null) return 0;
    return Geolocator.distanceBetween(
      state.currentPosition!.latitude, state.currentPosition!.longitude,
      target.latitude, target.longitude,
    );
  }
}

Google Maps güçlü ama API maliyeti vardır; flutter_map ücretsiz ve açık kaynak ama bazı özellikler için ek paket gerekir. Konum izinlerini hem Android (ACCESS_FINE_LOCATION) hem iOS (NSLocationWhenInUseUsageDescription) için doğru yapılandırın. Pil tüketimini azaltmak için distanceFilter değerini kullanım senaryosuna göre ayarlayın.