flutter_map icon indicating copy to clipboard operation
flutter_map copied to clipboard

[BUG] Slow loading and SocketExceptions for map tiles

Open CvisionTeam opened this issue 1 year ago • 19 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

CvisionTeam avatar Feb 19 '24 16:02 CvisionTeam

Hi @CvisionTeam, thanks for the report.

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

JaffaKetchup avatar Feb 19 '24 16:02 JaffaKetchup

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

yezouagh avatar Feb 20 '24 08:02 yezouagh

Please see https://docs.fleaflet.dev/layers/tile-layer/tile-providers#cancellablenetworktileprovider. Just install and swap out the provider.

JaffaKetchup avatar Feb 20 '24 17:02 JaffaKetchup

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 avatar Feb 20 '24 20:02 yezouagh

@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.

JaffaKetchup avatar Feb 20 '24 22:02 JaffaKetchup

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.

yezouagh avatar Feb 21 '24 09:02 yezouagh

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

bramp avatar Feb 21 '24 15:02 bramp

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']
  }

yezouagh avatar Feb 22 '24 09:02 yezouagh

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.

josxha avatar Feb 22 '24 10:02 josxha

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.

JaffaKetchup avatar Feb 23 '24 23:02 JaffaKetchup

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),
        ],
      ),
    );
  }
}

DiyorjonNasriddinov avatar Mar 13 '24 22:03 DiyorjonNasriddinov

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 ?

palicka avatar Mar 28 '24 09:03 palicka

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.

JaffaKetchup avatar Mar 29 '24 12:03 JaffaKetchup

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.

palicka avatar Mar 30 '24 12:03 palicka

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

JaffaKetchup avatar Mar 30 '24 12:03 JaffaKetchup

@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)

https://github.com/fleaflet/flutter_map/assets/6105846/dd5c9689-04c8-41fc-8d65-b6532a0e665b

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 avatar Apr 03 '24 18:04 palicka

@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 avatar Apr 03 '24 18:04 JaffaKetchup

@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...

palicka avatar Apr 03 '24 20:04 palicka

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.

ethicnology avatar May 18 '24 14:05 ethicnology

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.

JaffaKetchup avatar Aug 08 '24 21:08 JaffaKetchup