Vuco
Vuco

Reputation: 75

Flutter iOS - cannot get current location for initialCameraPosition before loading Google Maps widget

I'm trying to implement a simple map functionality in my Flutter iOS aplication. I followed guides starting from Flutter location package readme, through Medium "Implement Real-time Location Updates on Google Maps in your Flutter Apps" article, ending on many stack questions regarding similar issues.

The problem is that I cannot get current location before loading the maps, therefore getting its LatLng in initialCameraPosition results in NPE. What I can do is loading the maps with hardcoded initialCameraPosition, current location loads correctly then and I can move camera to it either by dragging or by clicking the floating location button in the right-bottom corner.

Here is my code sample:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:location/location.dart';

const double CAMERA_ZOOM = 16;
const LatLng SOURCE_LOCATION = LatLng(37.3317, -122.0325086);

class MapPage extends StatefulWidget {
  @override
  _MapPageState createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
  GoogleMapController mapController;
  LocationData currentLocation;
  Location location;
  bool _serviceEnabled;
  PermissionStatus _permissionGranted;

  @override
  void initState() {
    super.initState();
    location = new Location();

    location.onLocationChanged.listen((event) {
      currentLocation = event;
    });

    setInitialLocation();
  }

  void setInitialLocation() async {
    _serviceEnabled = await location.serviceEnabled();
    if (!_serviceEnabled) {
      _serviceEnabled = await location.requestService();
      if (!_serviceEnabled) {
        return;
      }
    }

    _permissionGranted = await location.hasPermission();
    if (_permissionGranted == PermissionStatus.denied) {
      _permissionGranted = await location.requestPermission();
      if (_permissionGranted != PermissionStatus.granted) {
        return;
      }
    }

    currentLocation =  await location.getLocation();
  }

  void _onMapCreated(GoogleMapController controller) {
    mapController = controller;
  }


  CameraPosition initialCameraPosition() {
    LatLng target;
    if (currentLocation != null)
      target = LatLng(currentLocation.latitude, currentLocation.longitude);
    else
      target = SOURCE_LOCATION;
    return CameraPosition(target: target, zoom: CAMERA_ZOOM);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.orangeAccent,
        title: Text('Map'),
        centerTitle: true,
      ),
      body:
      GoogleMap(
      onMapCreated: _onMapCreated,
      initialCameraPosition: initialCameraPosition(),
      myLocationEnabled: true,
      ),
    );
  }
}

I understand that getting current location is an asynchronous process, so I also tried to use FutureBuilder in the body section:

      FutureBuilder<LocationData>(
        future: currentLocation,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            LocationData currentLocation = snapshot.data;
            return GoogleMap(
              onMapCreated: _onMapCreated,
              initialCameraPosition: CameraPosition(
                  target: LatLng(
                      currentLocation.latitude, currentLocation.longitude)),
              myLocationEnabled: true,
            );
          } else if (snapshot.hasError) {
            return (Text("${snapshot.error}"));
          }
          return Center(
            child: CircularProgressIndicator(
              backgroundColor: Colors.orangeAccent,
            ),
          );
        },
      ),

which worked like a charm when I had to retrieve data from an http GET request, but this time it results only in an endless CircularProgressIndicator.

I tried many other solutions from stack questions, including returning empty Container when currentLocation is null, creating conditions for FutureBuilder snapshot.connectionState, again with no success.

Funny thing is that when I dug deeper into the Location.getCurrentLocation() method I found:

/// Gets the current location of the user.
  ///
  /// Throws an error if the app has no permission to access location.
  /// Returns a [LocationData] object.
  Future<LocationData> getLocation() async {
    return LocationPlatform.instance.getLocation();
  }

which then goes to:

/// Gets the current location of the user.
  ///
  /// Throws an error if the app has no permission to access location.
  /// Returns a [LocationData] object.
  Future<LocationData> getLocation() {
    throw UnimplementedError();
  }

which puts me in doubt if this plugin even works at all.

Here is my flutter doctor -v result:

[✓] Flutter (Channel master, v1.18.0-6.0.pre.82, on Mac OS X 10.15.4 19E287, locale pl-PL)
    • Flutter version 1.18.0-6.0.pre.82 at /Users/Vuco/flutter
    • Framework revision f35b673f2b (26 hours ago), 2020-04-19 02:45:01 +0530
    • Engine revision a5e0b2f2f2
    • Dart version 2.9.0 (build 2.9.0-1.0.dev 5b19445d9c)


[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
    • Android SDK at /Users/Vuco/Library/Android/sdk
    • Platform android-29, build-tools 29.0.3
    • Java binary at: /Users/Vuco/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6308749/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.4.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.4.1, Build version 11E503a
    • CocoaPods version 1.9.1

[✓] Android Studio (version 3.6)
    • Android Studio at /Users/Vuco/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6308749/Android Studio.app/Contents
    • Flutter plugin version 45.0.1
    • Dart plugin version 192.7761
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)

[!] IntelliJ IDEA Ultimate Edition (version 2020.1)
    • IntelliJ at /Users/Vuco/Applications/JetBrains Toolbox/IntelliJ IDEA Ultimate.app
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
    • For information about installing plugins, see
      https://flutter.dev/intellij-setup/#installing-the-plugins

[✓] VS Code (version 1.44.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.9.1

[✓] Connected device (1 available)
    • iPhone 8 • F60856B8-2C6E-401D-A0F9-06FFC7E09876 • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-4 (simulator)

! Doctor found issues in 1 category.

and the dependencies I use:

google_maps_flutter: ^0.5.25+3
location: ^3.0.2

Upvotes: 3

Views: 3150

Answers (2)

wcyankees424
wcyankees424

Reputation: 2654

The problem you are rendering your map before you have your initial position which I believe you realize this a FutureBuilder would work but might be over kill. All you really need is a Boolean.

GoogleMapController mapController;
  LocationData currentLocation;
  Location location;
  bool _serviceEnabled;
  PermissionStatus _permissionGranted;
  CameraPosition _center =
      CameraPosition(target: SOURCE_LOCATION, zoom: CAMERA_ZOOM);
  bool isLoading = false;

  @override
  void initState() {
    super.initState();
    location = new Location();

    _getLocation();
  }

  _getLocation() async {
    setState(() {
      isLoading = true;
    });
    initialize();
    currentLocation = await location.getLocation();
    if (currentLocation == null) {
      return;
    }
    _center = CameraPosition(
        target: LatLng(currentLocation.latitude, currentLocation.longitude),
        zoom: CAMERA_ZOOM);

    setState(() {
      isLoading = false;
    });

    print("CurrentLocation: $currentLocation");
  }

  Future<void> initialize() async {
    _serviceEnabled = await location.serviceEnabled();
    if (!_serviceEnabled) {
      _serviceEnabled = await location.requestService();
      if (!_serviceEnabled) {
        return;
      }
    }

    _permissionGranted = await location.hasPermission();
    if (_permissionGranted == PermissionStatus.DENIED) {
      _permissionGranted = await location.requestPermission();
      if (_permissionGranted != PermissionStatus.GRANTED) {
        return;
      }
    }
  }

  void _onMapCreated(GoogleMapController controller) {
    mapController = controller;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.orangeAccent,
        title: Text('Map'),
        centerTitle: true,
      ),
      body: isLoading
          ? CircularProgressIndicator()
          : Container(
              child: GoogleMap(
                onMapCreated: _onMapCreated,
                initialCameraPosition: _center,
                myLocationEnabled: true,
              ),
            ),
    );
  }

Upvotes: 2

Jwildsmith
Jwildsmith

Reputation: 1035

I would recommend using the Geolocator package instead. It is what I use in all the apps I need to get location and subscribe to locations in and it is well documented. Try using it and if you are still having issues with your FutureBuilder with that package update the post with your new code and I can try and help you further.

The FutureBuilder is the way to go though if you are not initializing and retrieving the location before the map is being built.

Upvotes: 0

Related Questions