fleaflet / flutter_map

A versatile mapping package for Flutter. Simple and easy to learn, yet completely customizable and configurable, it's the best choice for mapping in your Flutter app.

Home Page:https://pub.dev/packages/flutter_map

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[BUG] Slow loading and SocketExceptions for map tiles

CvisionTeam opened this issue · comments

What is the bug?

I am encountering issues with the Flutter flutter_map plugin when attempting to load tile images from Google Maps or OpenStreetMaps. Initially, the tile images load very slowly, and eventually, errors such as the following are raised:

═══════ Exception caught by image resource service ════════════════════════════ 
Connection closed before full header was received, uri=http://mt3.google.com/vt/lyrs=m&x=243&y=204&z=9&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff

and

═══════ Exception caught by image resource service ════════════════════════════
The following _ClientSocketException was thrown resolving an image codec:
ClientException with SocketException: Connection attempt cancelled, host: mt3.google.com, port: 80, uri=http://mt3.google.com/vt/lyrs=m&x=15841&y=12906&z=15&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff

When the exception was thrown, this was the stack:
#0      IOClient.send (package:http/src/io_client.dart:154:7)
io_client.dart:154
<asynchronous suspension>
#1      RetryClient.send (package:http/retry.dart:115:20)
retry.dart:115
<asynchronous suspension>
#2      BaseClient._sendUnstreamed (package:http/src/base_client.dart:93:32)
base_client.dart:93
<asynchronous suspension>
#3      BaseClient.readBytes (package:http/src/base_client.dart:58:22)
base_client.dart:58
<asynchronous suspension>
#4      FlutterMapNetworkImageProvider._loadAsync (package:flutter_map/src/layer/tile_layer/tile_provider/network_image_provider.dart:84:11)
network_image_provider.dart:84
<asynchronous suspension>
#5      MultiFrameImageStreamCompleter._handleCodecReady (package:flutter/src/painting/image_stream.dart:969:3)
image_stream.dart:969
<asynchronous suspension>

URL: http://mt3.google.com/vt/lyrs=m&x=15841&y=12906&z=15&apistyle=s.t%3A17|s.e%3Alg|p.v%3Aoff
Fallback URL: null
Current provider: FlutterMapNetworkImageProvider()
════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by image resource service ════════════════════════════
ClientException with SocketException: Connection attempt cancelled, host: mt3.google.com, port: 80, uri=http://mt3.google.com/vt/lyrs=m&x=15840&y=12903&z=15&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff
════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by image resource service ════════════════════════════
ClientException with SocketException: Connection attempt cancelled, host: mt3.google.com, port: 80, uri=http://mt3.google.com/vt/lyrs=m&x=15841&y=12902&z=15&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff
════════════════════════════════════════════════════════════════════════════════

These error started to rise after I updated to Flutter_map 6.1.0, Consequently upgraded the flutter,
I have confirmed that my internet connection is stable, and I have set the necessary network permissions in AndroidManifest.xml for both debug and release modes.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Here is my flutter doctor output:

Doctor summary (to see all details, run flutter doctor -v): 
[√] Flutter (Channel stable, 3.16.9, on Microsoft Windows [Version 10.0.19045.3930], locale en-US) 
[√] Windows Version (Installed version of Windows is version 10 or higher) 
[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0) 
[√] Chrome - develop for the web 
[√] Visual Studio - develop Windows apps (Visual Studio Build Tools 2022 17.3.6) 
[√] Android Studio (version 2023.1) 
[√] VS Code (version 1.86.1) 
[√] Connected device (4 available) 
[√] Network resources

• No issues found!

I have tested this issue on both real devices (Android, iPhone) and simulators (Android, iPhone), and the problem persists across all platforms.

Here is the relevant part of the build function for the screen:

Widget build(BuildContext context) {
  return Scaffold(
    body: Stack(
      children: [
        Container(
          color: Colors.white,
          child: FlutterMap(
            key: GlobalObjectKey("gmap"),
            mapController: mapController,
            options: mapOptions(),
            children: <Widget>[
              TileLayer(
                urlTemplate: 'http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&apistyle=s.t%3A17|s.e%3Alg|p.v%3Aoff',
                subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
              ),
              MarkerLayer(markers: markers),
            ],
          ),
        ), 
      ],
    ),
  );
}

Here are the relevent dependencies:

|-- flutter_map 6.1.0
|   |-- async 2.11.0
|   |   |-- collection...
|   |   '-- meta...      
|   |-- latlong2 0.9.0   
|   |   '-- intl...      
|   |-- logger 2.0.2+1   
|   |-- polylabel 1.0.1  
|   |   '-- collection...
|   |-- proj4dart 2.1.0  
|   |   |-- mgrs_dart 2.0.0
|   |   |   '-- unicode 0.3.1
|   |   |       '-- lists 1.0.1
|   |   |           '-- meta...
|   |   |-- wkt_parser 2.0.0
|   |   '-- meta...
|   |-- collection...
|   |-- flutter...
|   |-- http...
|   |-- meta...
|   '-- vector_math...
|-- flutter_map_marker_cluster 1.3.4
|   |-- flutter_map_marker_popup 6.1.2
|   |   |-- animated_stack_widget 0.0.4
|   |   |   '-- flutter...
|   |   |-- flutter...
|   |   |-- flutter_map...
|   |   |-- latlong2...
|   |   '-- provider...
|   |-- flutter...
|   |-- flutter_map...
|   '-- latlong2...
|-- http 1.2.0
|   |-- http_parser 4.0.2
|   |   |-- typed_data 1.3.2
|   |   |   '-- collection...
|   |   |-- collection...
|   |   |-- source_span...
|   |   '-- string_scanner...
|   |-- async...
|   |-- meta...
|   '-- web...
|-- http_client 1.5.3
|   |-- buffer 1.2.2
|   |-- executor 2.2.3
|   |   '-- stack_trace...
|   '-- http...
|-- intl 0.18.1
|   |-- clock 1.1.1
|   |-- path 1.8.3
|   '-- meta...

Any insights or suggestions on resolving this issue would be greatly appreciated. Thank you in advance for your assistance!

How can we reproduce it?

You can reproduce the error by updating flutter_map to 6.1.0 and flutter to latest release

Do you have a potential solution?

No response

Platforms

all platforms

Severity

Minimum: Allows normal functioning

Hi @CvisionTeam, thanks for the report.

Can you try using the cancellable tile provider and checking whether the issue persists?

Hi @JaffaKetchup, How can I do that, can you please provide an example?

Thank you for the update, but the problem persists even after I set the tileProvider: CancellableNetworkTileProvider().
I no longer see the errors in the console, but most of the tile images do not load or take too much time to load, please check the images below.

@yezouagh Sorry, I'm a little confused, are you related to the @CvisionTeam, who reported this issue initially? You look to be not using Google Maps, as in the initial post, in which case I would ask you check the speed of your tile server though a different library/on another site.

Hey @JaffaKetchup, this is my personal GitHub, the @CvisionTeam is my work GitHub account.

Yes, I'm also using Google Maps, I have a button for switching between different tile providers.
Now I noticed new errors:

I/flutter ( 5059): DioException [unknown]: null
I/flutter ( 5059): Error: HttpException: Connection closed before response was received, uri = http://mt1.google.com/vt/lyrs=m&x=498&y=403&z=10&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff
I/flutter ( 5059): DioException [unknown]: null
I/flutter ( 5059): Error: HttpException: Connection closed before response was received, uri = http://mt2.google.com/vt/lyrs=m&x=497&y=409&z=10&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff
I/flutter ( 5059): DioException [unknown]: null
I/flutter ( 5059): Error: HttpException: Connection closed before response was received, uri = http://mt3.google.com/vt/lyrs=m&x=500&y=403&z=10&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff
I/flutter ( 5059): DioException [unknown]: null
I/flutter ( 5059): Error: HttpException: Connection closed before response was received, uri = http://mt0.google.com/vt/lyrs=m&x=500&y=400&z=10&apistyle=s.t%3A17%7Cs.e%3Alg%7Cp.v%3Aoff
I/flutter ( 5059): DioException [unknown]: null

below are the screenshots of Google Maps tiles.

Random comment. Why are the Google Maps URLs http not https?

I didn't pay attention to that, I've always been using it like that, I don't this is the problem, because the other map tiles use HTTPS, and yet the problem arises.
These are the tiles providers I use:
Note: I added the HTTPS to the Google Maps Tiles URLs.

{
    'url':
        'https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&apistyle=s.t%3A17|s.e%3Alg|p.v%3Aoff',
    'sub': ['mt0', 'mt1', 'mt2', 'mt3']
  },
  {
    'url':
        'https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}&apistyle=s.t%3A17|s.e%3Alg|p.v%3Aoff',
    'sub': ['mt0', 'mt1', 'mt2', 'mt3']
  },
  {
    'url':
        'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
    'sub': ['']
  },
  {
    'url':
        'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
    'sub': ['abcd']
  },
  {
    'url':
        'https://{s}.google.com/vt/lyrs=m,traffic,transit&x={x}&y={y}&z={z}&apistyle=s.t%3A17|s.e%3Alg|p.v%3Aoff&hl=fr',
    'sub': ['mt0', 'mt1', 'mt2', 'mt3']
  }

I find two things difficult to understand:

  • Your first series of screenshots was taken over ~2 minutes, the second series over ~1 mimute. Are these recorded for the same loading or were you constantly zooming in and out?
  • Your example code includes one TileLayer while your json in the last comment implies 5 TileLayers. How many and what layers are you using?

Please provide either the code for the complete flutter_map widget with all layers and the MapOptions. A video showcasing the behaviour would be helpful too, the screenshots are hard to follow along and make conclusions on.

It could also be worth testing on the 'master' branch. #1742 was designed to fixed a similar issue, but is not available in a release yet, AFAIK.

Hello everyone, I had the same problem too.
I noticed a problem appeared when I call setState()
I think this is due to the TileLayer is being recreated
After that tile layer made a variable that is not recreated, my problem solved

Before:

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

  @override
  State<MapWidget> createState() => _MapWidgetState();
}

class _MapWidgetState extends State<MapWidget> {
  List<Polygon> polygons = [];
  List<Polyline> polylines = [];
  List<Marker> markers = [];

  @override
  Widget build(BuildContext context) {
    final bloc = geoDataCubit;

    return BlocListener<GeoDataCubit, GeoDataState>(
      listener: (context, state) {
        if (state is GeoDataLoaded) {
          polygons = state.polygons;
          polylines = state.polylines;
          markers = state.markers;
        }
        setState(() {});
      },
      child: FlutterMap(
        mapController: bloc.mapController,
        options: MapOptions(
          initialCenter: const LatLng(40.246973, 64.3),
          initialZoom: bloc.zoomLavel,
          cameraConstraint: CameraConstraint.contain(
            bounds: LatLngBounds(
              const LatLng(-90, -180),
              const LatLng(90, 180),
            ),
          ),
          interactionOptions: const InteractionOptions(
            flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
          ),
          onMapEvent: (MapEvent event) {
            bloc.onMapMoved(event.camera.zoom);
          },
        ),
        children: [
          TileLayer(
            urlTemplate: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
            userAgentPackageName: 'uz.ekon.mobile',
          ),
          PolylineLayer(polylines: polylines),
          PolygonLayer(polygons: polygons),
          MarkerLayer(markers: markers),
        ],
      ),
    );
  }
}

After:

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

  @override
  State<MapWidget> createState() => _MapWidgetState();
}

class _MapWidgetState extends State<MapWidget> {
  List<Polygon> polygons = [];
  List<Polyline> polylines = [];
  List<Marker> markers = [];
  final tileLayer = TileLayer(
    urlTemplate: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
    userAgentPackageName: 'uz.ekon.mobile',
  );

  @override
  Widget build(BuildContext context) {
    final bloc = geoDataCubit;

    return BlocListener<GeoDataCubit, GeoDataState>(
      listener: (context, state) {
        if (state is GeoDataLoaded) {
          polygons = state.polygons;
          polylines = state.polylines;
          markers = state.markers;
        }
        setState(() {});
      },
      child: FlutterMap(
        mapController: bloc.mapController,
        options: MapOptions(
          initialCenter: const LatLng(40.246973, 64.3),
          initialZoom: bloc.zoomLavel,
          cameraConstraint: CameraConstraint.contain(
            bounds: LatLngBounds(
              const LatLng(-90, -180),
              const LatLng(90, 180),
            ),
          ),
          interactionOptions: const InteractionOptions(
            flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
          ),
          onMapEvent: (MapEvent event) {
            bloc.onMapMoved(event.camera.zoom);
          },
        ),
        children: [
          tileLayer,
          PolylineLayer(polylines: polylines),
          PolygonLayer(polygons: polygons),
          MarkerLayer(markers: markers),
        ],
      ),
    );
  }
}

It could also be worth testing on the 'master' branch. #1742 was designed to fixed a similar issue, but is not available in a release yet, AFAIK.

I can confirm 'master' branch works. Thank you 👍 When we can expect this to be released ? I believe it will fix couple of issues reported recently (including this one) For me it's release blocker, UX is really bad. Also my app depends on packages that depend on flutter_map and can't be used when I use git dependency of flutter_map. (Also looks like master branch contains various undocumented(yet) API changes) Thank you very much
It's better, but empty squares ale still present sometimes. I don't use Cancelable tiles, does it affect this ?

Hi @palicka,
This looks like you might be rebuilding the map a lot. Can you post some code of your map and it's wrappers?
Also, image handling is noticably slower when in debug mode, so I would recommend testing in profile mode to see if that helps as well.

Hi @palicka, This looks like you might be rebuilding the map a lot. Can you post some code of your map and it's wrappers? Also, image handling is noticably slower when in debug mode, so I would recommend testing in profile mode to see if that helps as well.

thank you, I removed unnecessary animations, added CancellableNetworkTileProvider and it looks better.

If animations were causing the issue, that suggests it could be frequent rebuilding that's the issue.

@JaffaKetchup I thought it's better, but it's still not, I think. Sometimes it feels ok, but when I move the map really fast it's not as smooth as it used to be..Lot's of missing squares. To fix it, I need to move around that area slowly, zoom-in, zoom-out to re-load it. It doesn't matter if I use wifi or 5G, android/iphone/emulator, OSM data or mine, it behaves the same.. Please, have a look at the video and let me know what you think(In this example I was just using fingers to move the map, but in real example I also use animation(from flutter_map example) from one point to another, and it's basically the same, so I didn't attach it here, to keep the code as minimal as possible) (it's built in release mode, not debug)

test.mov

here is the full code:

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart';
import 'package:latlong2/latlong.dart' as latLng;

class MapScreenTest extends StatefulWidget {
  @override
  MapScreenTestState createState() => MapScreenTestState();
}

class MapScreenTestState extends State<MapScreenTest> {
  MapController mapController = MapController();

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Stack(
        children: [
          FlutterMap(
            options: MapOptions(
              minZoom: 7,
              maxZoom: 15,
              cameraConstraint: CameraConstraint.containCenter(
                  bounds: LatLngBounds(latLng.LatLng(50.7003884, 23.7870953),
                      latLng.LatLng(46.050174, 13.599262))),
              initialCenter: latLng.LatLng(48.826594, 19.618809),
              initialZoom: 5,
              backgroundColor: Colors.black,
              interactionOptions: InteractionOptions(
                  flags: InteractiveFlag.doubleTapZoom |
                      InteractiveFlag.drag |
                      InteractiveFlag.flingAnimation |
                      InteractiveFlag.pinchMove |
                      InteractiveFlag.pinchZoom),
            ),
            children: [
              TileLayer(
                  urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
                  tileProvider: CancellableNetworkTileProvider()),
            ],
            mapController: mapController,
          ),
          Column(
            children: [
              Padding(
                padding: EdgeInsets.all(10),
                child: GestureDetector(
                  onTap: () {
                    mapController.move(latLng.LatLng(50.7003884, 23.7870953), 15);
                  },
                  child: Text("ZOOM IN"),
                ),
              ),
              Padding(
                padding: EdgeInsets.all(10),
                child: GestureDetector(
                  onTap: () {
                    mapController.move(latLng.LatLng(50.7003884, 23.7870953), 7);
                  },
                  child: Text("ZOOM OUT"),
                ),
              ),
            ],
          )
        ],
      ),
    );
  }
}

flutter SDK: 3.19.1
flutter_map: ^6.1.0
flutter_map_cancellable_tile_provider: ^2.0.0

I see lot's of various exceptions in the console (I don't know if it's related just to CancellableNetworkTileProvider):

DioException [unknown]: null
Error: HttpException: Connection closed before response was received, uri = https://tile.openstreetmap.org/13/4543/2754.png 
DioException [unknown]: null
Error: HttpException: Connection closed before response was received, uri = https://tile.openstreetmap.org/13/4539/2756.png

or 

DioException [connection error]: The connection errored: Connection failed This indicates an error which most likely cannot be solved by the library.
Error: SocketException: Connection failed (OS Error: Network is unreachable, errno = 101), address = tile.openstreetmap.org, port = 443

This error is not that frequent, but appears sometimes :

Failed to decode image
android.graphics.ImageDecoder$DecodeException: Failed to create image decoder with message 'unimplemented'Input contained an error.
at android.graphics.ImageDecoder.nCreate(Native Method)
at android.graphics.ImageDecoder.-$$Nest$smnCreate(Unknown Source:0)
at android.graphics.ImageDecoder$ByteBufferSource.createImageDecoder(ImageDecoder.java:242)
at android.graphics.ImageDecoder.decodeBitmapImpl(ImageDecoder.java:2015)
at android.graphics.ImageDecoder.decodeBitmap(ImageDecoder.java:2008)
at io.flutter.embedding.engine.g.a(SourceFile:1)
at io.flutter.embedding.engine.FlutterJNI.decodeImage(SourceFile:17)

@palicka Can you try moving the CancellableNetworkTileProvider() into a variable instantiated outside of the build method? Something like final ctp = ...;, then just tileProvider: ctp.

@JaffaKetchup I moved it, and no change. Then I removed CancellableNetworkTileProvider and move TileLayer into a variable as well, as it was mentioned in a comment above, and then it looked much better - only when used in the minimum code example. When I tried it within my real code, it didn't help, but then I found out there are indeed unnecessary rebuilds sometimes (other than before), I need to re-write it and we will see...
To sum it up, I think there are two issues here - unnecessary rebuilds within my app and tiles not refreshing when using CancellableNetworkTileProvider...
EDIT: migrating back to v5 fixed the issue...

OpenStreetMap & MapBox user here.

Removing CancellableNetworkTileProvider seems to help

No more errors thrown in the logs:

flutter: DioException [connection error]: The connection errored: Dio can't establish a new connection after it was closed. This indicates an error which most likely cannot be solved by the library.

I'm going to close this for now, as there are too many factors here. Occasionally I run into similar slow loading, but I'm not convinced it's flutter_map's fault: on these occasions, I'm trying to do a lot of network requests at once, so maybe something else can't handle it/has to stagger it.

If people still experience this, please open a new issue with an MRE using the OpenStreetMap tile server.