Search code examples
fluttergoogle-mapsdartdisposestatefulwidget

Flutter: setState() called after dispose() error


This is actually a working Google Map with with a google place search. The problem is upon disposing this Stateful Widget, I am getting this error:

E/flutter ( 6017): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: setState() called after dispose(): _MapScreenState#79287(lifecycle state: defunct, not mounted, tickers: tracking 0 tickers) E/flutter ( 6017): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. E/flutter ( 6017): The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree. E/flutter ( 6017): This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().

Can somebody help me identify what did I miss? btw here's my code snippet:

import 'package:CaterChive/model/configKey.dart';
import 'package:CaterChive/services/hiveController.dart';
import 'package:CaterChive/style/textStyles.dart';
import 'package:CaterChive/view/homeScreen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:geocoder/geocoder.dart';
import 'package:geocoder/model.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:hive/hive.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:search_map_place_v2/search_map_place_v2.dart';

import 'package:http/http.dart' as http;
import 'dart:convert';

class MapScreen extends StatefulWidget {
  @override
  _MapScreenState createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> with TickerProviderStateMixin {
  GoogleMapController _mapController;
  bool isMapCreated = false;
  String address = Hive.box('userLocation').get(1).userAddress;
  double mapLatitude = Hive.box('userLocation').get(1).addressLatitude;
  double mapLongitude = Hive.box('userLocation').get(1).addressLongitude;

  String selectedAddress;

  bool _hasClearButton = true;

  LatLng mapLocation = LatLng(Hive.box('userLocation').get(1).addressLatitude,
      Hive.box('userLocation').get(1).addressLongitude);

  int mapRadius = 30000;
  bool strictBounds = false;
  PlaceType mapPlaceType = PlaceType.address;
  String language = 'en';

  final _textEditingController = TextEditingController();
  AnimationController _animationController;
  // SearchContainer height.
  // Animation _containerHeight;
  // Place options opacity.
  Animation _listOpacity;

  List<dynamic> _placePredictions = [];
  bool _isEditing = false;
  Geocoding geocode;

  String _tempInput = '';
  String _currentInput = '';

  final _fn = FocusNode();

  CrossFadeState _crossFadeState;

  bool locBusy = false;
  LatLng newLocation;
  String newLocationText;

  mapStyle() {
    getJsonFile('assets/json/mapStyle.json').then(setMapStyle);
  }

  Future<String> getJsonFile(String path) async {
    return await rootBundle.loadString(path);
  }

  void setMapStyle(String mapStyle) {
    _mapController.setMapStyle(mapStyle);
  }

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

  void fetchData() {
    print(mapLatitude);
    print(mapLongitude);
    if (isMapCreated) {
      mapStyle();
    }

    geocode = Geocoding(apiKey: mapKey, language: 'en');
    _animationController = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 500));
    // _containerHeight = Tween<double>(begin: 0, end: 364).animate(
    //   CurvedAnimation(
    //     curve: const Interval(0.0, 0.5, curve: Curves.easeInOut),
    //     parent: _animationController,
    //   ),
    // );
    _listOpacity = Tween<double>(
      begin: 0,
      end: 1,
    ).animate(
      CurvedAnimation(
        curve: const Interval(0.5, 1.0, curve: Curves.easeInOut),
        parent: _animationController,
      ),
    );

    _textEditingController.addListener(_autocompletePlace);
    customListener();

    if (_hasClearButton) {
      _fn.addListener(() async {
        if (_fn.hasFocus) {
          setState(() => _crossFadeState = CrossFadeState.showSecond);
        } else {
          setState(() => _crossFadeState = CrossFadeState.showFirst);
        }
      });
      _crossFadeState = CrossFadeState.showFirst;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      resizeToAvoidBottomPadding: false,
      appBar: AppBar(
        centerTitle: true,
        backgroundColor: Color(0xff009245),
        title: Row(
          children: [
            GestureDetector(
                onTap: () {
                  Get.off(HomeScreen());
                },
                child: Icon(Icons.arrow_back)),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 15.0),
                child: Container(
                    height: 40,
                    decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius:
                            const BorderRadius.all(Radius.circular(6.0)),
                        boxShadow: [
                          const BoxShadow(
                              color: Colors.black12,
                              blurRadius: 20,
                              spreadRadius: 10)
                        ]),
                    child: Row(
                      children: [
                        Expanded(
                          child: TextField(
                            style: TextStyle(fontWeight: FontWeight.bold),
                            onTap: _textEditingController.text ==
                                    'Selected Location'
                                ? () {
                                    _textEditingController.clear();
                                  }
                                : () {},
                            controller: _textEditingController,
                            onSubmitted: (_) => _selectPlace(),
                            onEditingComplete: _selectPlace,
                            autofocus: false,
                            focusNode: _fn,
                            decoration: InputDecoration(
                                hintText: Hive.box('userCredentials')
                                            .get(1)
                                            .acctType ==
                                        1
                                    ? 'Set Delivery Address'
                                    : 'Set Location',
                                border: InputBorder.none,
                                contentPadding:
                                    const EdgeInsets.fromLTRB(10, 0, 0, 5),
                                hintStyle: TextStyle(
                                    color: Colors.grey[500],
                                    fontWeight: FontWeight.normal)),
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 8.0),
                          child: Container(
                            child: _hasClearButton
                                ? GestureDetector(
                                    onTap: () {
                                      if (_crossFadeState ==
                                          CrossFadeState.showSecond) {
                                        _textEditingController.clear();
                                      }
                                    },
                                    // child: Icon(_inputIcon, color: this.widget.iconColor),
                                    child: AnimatedCrossFade(
                                      crossFadeState: _crossFadeState,
                                      duration:
                                          const Duration(milliseconds: 300),
                                      firstChild: Icon(Icons.search,
                                          color: Color(0xff009245)),
                                      secondChild: Icon(Icons.clear,
                                          color: Color(0xff009245)),
                                    ),
                                  )
                                : Icon(Icons.search, color: Color(0xff009245)),
                          ),
                        ),
                      ],
                    )),
              ),
            ),
            GestureDetector(
                onTap: () {
                  checkLocationPermission();
                  print('tapped gps');
                },
                child: Icon(Icons.gps_fixed)),
          ],
        ),
      ),
      body: Stack(
        children: [
          GestureDetector(
            onTap: () {
              print('hello');
            },
            child: Container(
              height: double.infinity,
              width: double.infinity,
              child: GoogleMap(
                onCameraMoveStarted: _onCameraMoveStarted,
                onCameraMove: _onCameraMove,
                onCameraIdle: _onCameraIdle,
                onTap: _mapTap,
                onLongPress: _mapLongPress,
                zoomControlsEnabled: false,
                onMapCreated: (GoogleMapController googleMapController) {
                  _mapController = googleMapController;
                  isMapCreated = true;
                  mapStyle();
                  setState(() {});
                },
                initialCameraPosition: CameraPosition(
                    zoom: 18.0, target: LatLng(mapLatitude, mapLongitude)),
              ),
            ),
          ),

          Container(
            alignment: Alignment(0, 0),
            child: SvgPicture.asset(
              "assets/images/ccGPSshadow.svg",
              width: locBusy ? 15 : 23,
              fit: BoxFit.cover,
              alignment: Alignment.center,
            ),
          ),

          Container(
            alignment: Alignment(0, locBusy ? -0.1 : -0.06),
            child: SvgPicture.asset(
              "assets/images/ccGPS.svg",
              fit: BoxFit.cover,
              width: 28,
              alignment: Alignment.center,
            ),
          ),

          AnimatedBuilder(
              animation: _animationController,
              builder: (context, _) {
                return Container(
                  height: 65.0 * _placePredictions.length,
                  alignment: Alignment(0, -1),
                  decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: const BorderRadius.only(
                          bottomLeft: Radius.circular(6.0),
                          bottomRight: Radius.circular(6.0)),
                      boxShadow: [
                        const BoxShadow(
                            color: Colors.black12,
                            blurRadius: 20,
                            spreadRadius: 10)
                      ]),
                  child: Column(
                    children: <Widget>[
                      // Padding(
                      //   padding: const EdgeInsets.only(
                      //       left: 12.0, right: 12.0, top: 4),
                      //   child: child,
                      // ),
                      // if (_placePredictions.isEmpty)
                      Opacity(
                        opacity: _listOpacity.value,
                        child: Column(
                          children: <Widget>[
                            for (var prediction in _placePredictions)
                              _placeOption(Place.fromJSON(prediction, geocode)),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              }),

          Container(
            alignment: Alignment(0, 0.9),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 15),
              child: MaterialButton(
                minWidth: double.infinity,
                onPressed: locBusy
                    ? () {}
                    : () async {
                        if (mapLocation.isNullOrBlank) {
                          print('empty');
                          Get.off(HomeScreen());
                        } else {
                          // convert coordinates to valid address string
                          final coordinates = Coordinates(
                              mapLocation.latitude, mapLocation.longitude);

                          var addresses = await Geocoder.local
                              .findAddressesFromCoordinates(coordinates);
                          address = addresses.first.addressLine;
                          print(address);
                          await HiveController().putLocationOnHive(address,
                              mapLocation.latitude, mapLocation.longitude);
                          Get.off(HomeScreen());
                        }
                      },
                child: Padding(
                    padding: const EdgeInsets.all(15.0),
                    child: Text(
                      locBusy ? 'Loading...' : 'Find Nearby Services',
                      style: lblBtn,
                    )),
                color: locBusy ? Colors.grey : Color(0xff009245),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
          ),

          // Container(
          //   alignment: Alignment(0, -0.9),
          //   child: Padding(
          //     padding: const EdgeInsets.only(top: 15),
          //     child: Column(
          //       children: [
          //         SearchMapPlaceWidget(
          //           apiKey: mapKey,
          //           hasClearButton: true,
          //           location: LatLng(mapLatitude, mapLongitude),
          //           placeType: PlaceType.address,
          //           radius: 30000,
          //           placeholder: 'Set delivery address',
          //           onSelected: (Place place) async {
          // Geolocation geolocation = await place.geolocation;
          // _mapController.animateCamera(
          //     CameraUpdate.newLatLng(geolocation.coordinates));
          // _mapController.animateCamera(
          //     CameraUpdate.newLatLngBounds(geolocation.bounds, 0));

          // print(place.description);

          // var newLoc = await Geocoder.local
          //     .findAddressesFromQuery(place.description);
          // print(geolocation.coordinates);

          // mapLatitude = newLoc.first.coordinates.latitude;
          // mapLongitude = newLoc.first.coordinates.longitude;

          // print(mapLatitude);
          // print(mapLongitude);
          //           },
          //         ),
          //         FlatButton(
          //           child: Text(
          //             "Use Location",
          //             style: TextStyle(color: Colors.white),
          //           ),
          //           onPressed: () {
          //             print('Hi');
          //           },
          //         ),
          //       ],
          //     ),
          //   ),
          // ),
        ],
      ),
    );
  }

  /*
  WIDGETS
  */

  Widget _placeOption(Place prediction) {
    final place = prediction.description;

    return MaterialButton(
      padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
      onPressed: () => _selectPlace(prediction: prediction),
      child: ListTile(
        title: Text(
          place.length < 45
              ? '$place'
              : '${place.replaceRange(45, place.length, "")} ...',
          style: TextStyle(
            fontSize: MediaQuery.of(context).size.width * 0.04,
            // color: widget.darkMode ? Colors.grey[100] : Colors.grey[850],
          ),
          maxLines: 1,
        ),
        contentPadding: const EdgeInsets.symmetric(
          horizontal: 10,
          vertical: 0,
        ),
      ),
    );
  }

  /*
  METHODS
  */

  /// Will be called everytime the input changes. Making callbacks to the Places
  /// Api and giving the user Place options

  void _onCameraMoveStarted() {
    _fn.unfocus();
    _closeSearch();
    locBusy = true;
  }

  void _onCameraMove(CameraPosition position) {
    _closeSearch();
    newLocation = position.target;
    locBusy = true;

    _textEditingController.text = 'Selected Location';
    _fn.unfocus();
  }

  void _onCameraIdle() {
    if (newLocationText != '') _textEditingController.text = newLocationText;
    mapLocation = newLocation;
    print(mapLocation);
    locBusy = false;
    newLocationText = '';
  }

  void _mapTap(LatLng latlng) {
    _fn.unfocus();
    _closeSearch();
    print(latlng);

    _mapController.animateCamera(CameraUpdate.newLatLng(latlng));
    // _mapController.animateCamera(CameraUpdate.newLatLngBounds(
    //     LatLngBounds(
    //       southwest: latlng,
    //       northeast: latlng,
    //     ),
    //     100));
  }

  void _mapLongPress(LatLng latlng) {
    _fn.unfocus();
    _closeSearch();
    print(latlng);
  }

  void _autocompletePlace() async {
    if (_fn.hasFocus) {
      setState(() {
        _currentInput = _textEditingController.text;
        _isEditing = true;
      });

      _textEditingController.removeListener(_autocompletePlace);

      if (_currentInput.isEmpty) {
        // if (_currentInput != "Selected Location") _closeSearch();
        _textEditingController.addListener(_autocompletePlace);
        return;
      }

      if (_currentInput == _tempInput) {
        final predictions = await _makeRequest(_currentInput);
        await _animationController.animateTo(0.5);
        setState(() => _placePredictions = predictions);
        await _animationController.forward();

        _textEditingController.addListener(_autocompletePlace);
        return;
      }

      Future.delayed(const Duration(milliseconds: 500), () {
        _textEditingController.addListener(_autocompletePlace);
        if (_isEditing == true) _autocompletePlace();
      });
    }
  }

  Future<void> checkLocationPermission() async {
    var locationPermission = Permission.location;

    if (await locationPermission.request().isGranted) {
      _getCurrentLocation();
    } else if (await locationPermission.request().isDenied) {
      Fluttertoast.showToast(
        gravity: ToastGravity.CENTER,
        msg: "Location Permission Denied",
      );
    } else if (await locationPermission.request().isPermanentlyDenied) {
      Fluttertoast.showToast(
        gravity: ToastGravity.CENTER,
        msg:
            "Location Permission Permanently Denied, Cannot Request Permission",
      );
    } else {
      Fluttertoast.showToast(
        gravity: ToastGravity.CENTER,
        msg: "Please check location permission",
      );
    }
  }

  void _getCurrentLocation() async {
    // GoogleMapController _mapController;
    // Position position;

    // Position res = await Geolocator.getCurrentPosition();

    // position = res;

    Position geoposition = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);

    if (await Permission.locationWhenInUse.serviceStatus.isEnabled) {
      print('GPS enabled');

      print(geoposition.latitude);
      print(geoposition.longitude);

      final coordinates =
          Coordinates(geoposition.latitude, geoposition.longitude);

      print(coordinates);

      var addresses =
          await Geocoder.local.findAddressesFromCoordinates(coordinates);

      selectedAddress = addresses.first.addressLine;
      mapLatitude = geoposition.latitude;
      mapLongitude = geoposition.longitude;

      newLocationText = selectedAddress;

      _mapController.animateCamera(CameraUpdate.newLatLng(
          LatLng(geoposition.latitude, geoposition.longitude)));
      // _mapController.animateCamera(CameraUpdate.newLatLngBounds(
      //     LatLngBounds(
      //       southwest: LatLng(
      //           geoposition.latitude - 0.0002, geoposition.longitude - 0.0002),
      //       northeast: LatLng(
      //           geoposition.latitude + 0.0002, geoposition.longitude + 0.0002),
      //     ),
      //     100));

      // readOnly.value = false;
      // tapped.value = true;
    } else {
      Fluttertoast.showToast(
        gravity: ToastGravity.CENTER,
        msg: "Please turn on your GPS",
      );
    }
  }

  /// API request function. Returns the Predictions
  Future<dynamic> _makeRequest(input) async {
    var url =
        // ignore: unnecessary_brace_in_string_interps
        'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$input&key=${mapKey}&language=${language}';
    if (mapLocation != null && mapRadius != null) {
      url +=
          // ignore: unnecessary_brace_in_string_interps
          '&location=${mapLocation.latitude},${mapLocation.longitude}&radius=${mapRadius}';
      if (strictBounds) {
        url += '&strictbounds';
      }
      if (mapPlaceType != null) {
        url += '&types=${mapPlaceType.apiString}';
      }
    }

    final response = await http.get(url);
    final json = jsonDecode(response.body);

    if (json['error_message'] != null) {
      var error = json['error_message'];
      if (error == 'This API project is not authorized to use this API.') {
        error +=
            ' Make sure the Places API is activated on your Google Cloud Platform';
      }

      throw Exception(error);
    } else {
      final predictions = json['predictions'];
      return predictions;
    }
  }

  /// Will be called when a user selects one of the Place options
  void _selectPlace({Place prediction}) async {
    if (prediction != null) {
      // _textEditingController.value = TextEditingValue(
      //   text: prediction.description,
      //   selection: TextSelection.collapsed(
      //     offset: prediction.description.length,
      //   ),
      // );
      newLocationText = prediction.description;
    } else {
      await Future.delayed(const Duration(milliseconds: 500));
    }

    // Makes animation
    _closeSearch();

    // Calls the `onSelected` callback
    if (prediction is Place) onSelected(prediction);
  }

  onSelected(Place place) async {
    Geolocation geolocation = await place.geolocation;
    _mapController
        .animateCamera(CameraUpdate.newLatLng(geolocation.coordinates));
    // _mapController
    //     .animateCamera(CameraUpdate.newLatLngBounds(geolocation.bounds, 0));

    print(place.description);

    var newLoc = await Geocoder.local.findAddressesFromQuery(place.description);
    print(geolocation.coordinates);

    mapLatitude = newLoc.first.coordinates.latitude;
    mapLongitude = newLoc.first.coordinates.longitude;

    print(mapLatitude);
    print(mapLongitude);
  }

  /// Closes the expanded search box with predictions
  void _closeSearch() async {
    if (!_animationController.isDismissed) {
      await _animationController.animateTo(0.5);
    }

    _fn.unfocus();
    setState(() {
      _placePredictions = [];
      _isEditing = false;
    });
    await _animationController.reverse();
    _textEditingController.addListener(_autocompletePlace);
  }

  /// Will listen for input changes every 0.5 seconds, allowing us to make API requests only when the user stops typing.
  void customListener() {
    if (!mounted) {
      return;
    }
    Future.delayed(const Duration(milliseconds: 500), () {
      setState(() => _tempInput = _textEditingController.text);
      customListener();
    });
  }

  @override
  void dispose() {
    _mapController.dispose();
    _animationController.dispose();
    _textEditingController.dispose();
    _fn.dispose();
    super.dispose();
  }
}

Solution

  • This error implies that you are calling setState((){}) when the widget is no longer in the tree.

    I didn't check your whole code to get where is the problem exactly, but you can add this before calling setState((){}) :

    if(mounted){
      // mounted returns true only if the widget is in the tree
      setState((){
        // do your stuff
      });
    }
    

    Usually, this happens when there are some Futures, listeners, http calls or things like that, which you have.