osm_flutter icon indicating copy to clipboard operation
osm_flutter copied to clipboard

Map rebuilds on UI update

Open xt1zer opened this issue 3 years ago • 24 comments

I built an animation, which computes value for opacity and scale of a widget while scrolling pageview, and every frame I scroll it the map rebuilds and it's frustrating, eventually failing to an exception at one moment.

xt1zer avatar Sep 20 '21 22:09 xt1zer

in Android, we have some limitation, if the widget that you try to resized is the map try to avoid that and avoid rebuild parent widget for now until i will find solution for that

liodali avatar Sep 21 '21 06:09 liodali

Same issue. On ios it's ok but on android when screen update, map rebuild and fails. Please help !

thpresident avatar Oct 22 '21 16:10 thpresident

what did you mean by the map fails ?

liodali avatar Oct 22 '21 17:10 liodali

@thpresident @k0lourful I also had some issues with animation and other flickering and rebuild issues while using fake GPS to create routes. It generally occurs when using API level at or below 29. For emulators, I think API level 30 or above should work fine.

jesussmile avatar Nov 16 '21 12:11 jesussmile

can you provide me small example to see the problem

liodali avatar Nov 16 '21 12:11 liodali

I am sorry their problem may be different than mine. I have since moved on to tabpageview . I guess they will have to elaborate their problem more. For me API 29 causes rebuilt and freezing.

jesussmile avatar Nov 16 '21 13:11 jesussmile

hi all! any updates about problem?

Darkildo avatar Apr 08 '23 19:04 Darkildo

share your code to see ewactly your issue you need to separate the map from widget where you manipulate the dara

liodali avatar Apr 08 '23 20:04 liodali

I using OSMMap inside stateful widget, but when stateful widget is resized map always loading изображение изображение

Darkildo avatar Apr 09 '23 06:04 Darkildo

the some widget is widget that will be shown for specific situation you make above of the map try to set this androidHotReloadSupport to true but take on consideration that the map will restarted

liodali avatar Apr 09 '23 11:04 liodali

is there any way to avoid reloading the map when resizing it? it's pretty weird.

Darkildo avatar Apr 09 '23 11:04 Darkildo

for now in android side , we don't have a solution for that since our plugin use native platform its weird for us also that our native view lose the context when flutter view rebuilt

liodali avatar Apr 09 '23 12:04 liodali

@Darkildo can you post your code? I have resized the map before.. in my case I had a container with map on top and another widget at bottom. I would resize the bottom container and simulatiously resize the map. I dont think it reloaded... is this your issue or sth else?

jesussmile avatar Apr 09 '23 12:04 jesussmile

  final double width;
  final double height;

  const OSMMap({super.key, required this.width, required this.height});

  @override
  State<OSMMap> createState() => _OSMMapState();
}

class _OSMMapState extends State<OSMMap> {
  late OSM.MapController controller;
  final mapGlobalKey = GlobalKey();
  @override
  void initState() {
    super.initState();
    controller = OSM.MapController.withPosition(
      initPosition: OSM.GeoPoint(
        latitude: 80.45,
        longitude: 17.36,
      ),
    );
    controller.setZoom(zoomLevel: 14);
   
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        RepaintBoundary(
          child: OSM.OSMFlutter(
            key: mapGlobalKey,
            controller: controller,
            androidHotReloadSupport: true,
            mapIsLoading: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                mainAxisAlignment: MainAxisAlignment.center,
                children: const [
                  CircularProgressIndicator(),
                  Text("Map is Loading.."),
                ],
              ),
            ),
            onMapIsReady: (isReady) {
              if (isReady) {
                print("map is ready");
              }
            },
            onGeoPointClicked: (point) => print(point),
            // trackMyPosition: true,
          ),
        ),
        Align(
          alignment: Alignment.bottomRight,
          child: Padding(
            padding: EdgeInsets.only(
              right: 18.kH(context),
              bottom: 16.kH(context),
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Padding(
                  padding: EdgeInsets.only(bottom: 20.kH(context)),
                  child: MapButton(
                    isActive: context.watchChargeStationCubit.state.isVisible,
                    onTap: () {
                      context.chargeStationCubit.visibleToggled();
                      DI.metrics.L(
                        context.chargeStationCubit.state.isVisible
                            ? 'Hide charge stations'
                            : 'Show charge stations',
                      );
                    },
                    activeIconPath: Assets.icons.selectedChargeButton.path,
                    inactiveIconPath: Assets.icons.unselectedChargeButton.path,
                  ),
                ),
                CurrentLocationButton(
                  onPressed: () {
                    controller.currentLocation();
                    DI.metrics.L('Show current location');
                  },
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

изображение

Darkildo avatar Apr 09 '23 16:04 Darkildo

okay i am not entirely sure what is it you are looking for? but to avoid rebuilds try this example and let us knw ,is this something you want ? The same logic can be applied to PageView as well.. if this is not the issue take this code below and add a minimum of your issue to it as an example, and show where map is reloading.?

import 'package:flutter/material.dart';
import 'package:flutter_osm_plugin/flutter_osm_plugin.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late MapController controller;

  TabController? tabController;
  int selectedIndex = 0;
  void onItemClicked(int index) {
    setState(() {
      selectedIndex = index;
      tabController!.index = selectedIndex;
    });
  }

  @override
  void initState() {
    super.initState();
    tabController = TabController(length: 2, vsync: this);
  }

  @override
  void dispose() {
    super.dispose();
    tabController!.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TabBarView(
        physics: NeverScrollableScrollPhysics(),
        controller: tabController,
        children: const [
          HomeTabPage(),
          EarningsTabPage(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
          BottomNavigationBarItem(
              icon: Icon(Icons.credit_card), label: "Another Tab"),
        ],
        type: BottomNavigationBarType.fixed,
        selectedLabelStyle: const TextStyle(fontSize: 12.0),
        showUnselectedLabels: true,
        currentIndex: selectedIndex,
        onTap: onItemClicked,
      ),
    );
  }
}

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

  @override
  State<HomeTabPage> createState() => _HomeTabPageState();
}

class _HomeTabPageState extends State<HomeTabPage>
    with
        AutomaticKeepAliveClientMixin,
        // OSMMixinObserver,
        WidgetsBindingObserver {
  final GlobalKey _mapKey = GlobalKey();
  late MapController osmController;
  bool _isMapExpanded = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    osmController = MapController(
      initMapWithUserPosition: true,
    );

    // osmController.addObserver(this);
  }

  void toggleMapSize() {
    setState(() {
      _isMapExpanded = !_isMapExpanded;
    });
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
      appBar: AppBar(
        title: Text("resize"),
      ),
      body: PageStorage(
        bucket: PageStorageBucket(),
        child: Column(
          children: [
            Expanded(
              flex: _isMapExpanded ? 1 : 9,
              child: Container(
                height: double.infinity,
                child: OSMFlutter(
                  key: const PageStorageKey('myMap'),
                  controller: osmController,
                  trackMyPosition: false,
                  initZoom: 12,
                  minZoomLevel: 8,
                  maxZoomLevel: 14,
                  stepZoom: 1.0,
                  userLocationMarker: UserLocationMaker(
                    personMarker: const MarkerIcon(
                      icon: Icon(
                        Icons.location_history_rounded,
                        color: Colors.red,
                        size: 48,
                      ),
                    ),
                    directionArrowMarker: const MarkerIcon(
                      icon: Icon(
                        Icons.double_arrow,
                        size: 48,
                      ),
                    ),
                  ),
                ),
              ),
            ),
            Expanded(
              flex: 1,
              child: Center(
                child: ElevatedButton(
                  onPressed: toggleMapSize,
                  child: Text(_isMapExpanded ? 'Expand Map' : 'Shrink Map'),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

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

  @override
  State<EarningsTabPage> createState() => _EarningsTabPageState();
}

class _EarningsTabPageState extends State<EarningsTabPage> {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: const Center(
        child: Text(
          "container",
          style: (TextStyle(fontSize: 50, color: Colors.white)),
        ),
      ),
    );
  }
}

jesussmile avatar Apr 09 '23 19:04 jesussmile

you can put OSM.OSMFlutter in another statelessWidget to prevent it from rebuild

liodali avatar Apr 09 '23 19:04 liodali

Thank you guys for your help, I removed the androidHotReload flag and everything worked fine. Didn't even have to add RepaintBoundary and PageStorage

Darkildo avatar Apr 10 '23 04:04 Darkildo

oh yes! i remember it causing issues too, never used it much , works fine without it.

jesussmile avatar Apr 10 '23 05:04 jesussmile

any progress? I had a similar problem, once OSMFlutter was built it work fine, but when I called it again (after change of widget/route, etc, and coming back to build it again) the loading of OSMFlutter never was completed. Something that I did that solved it, was give a new value to the controller, maybe you can try?

    mapController = MapController(
      initPosition: GeoPoint(latitude: 0, longitude: 0)
    );

gabrielmarinhoindico avatar Jan 12 '24 12:01 gabrielmarinhoindico

can you share some sample and also on which version you're using ?

liodali avatar Jan 12 '24 13:01 liodali

can you share some sample and also on which version you're using ?

Explanation at the end (ignore some parts that maybe are not being used, i edited it to a way that I can share), some specifications:

  • Flutter Version: 3.10.5;

  • flutter_osm_interface - version: "0.5.2";

  • flutter_osm_plugin - version: "0.55.3";

  • flutter_osm_web - version: "0.4.2";

  • Android: Tests on Android 9 until Android 11

import 'package:flutter/material.dart';
import 'package:flutter_osm_plugin/flutter_osm_plugin.dart';

class RebuildProvider extends ChangeNotifier {
  RebuildProvider._();

  static final RebuildProvider _instance = RebuildProvider._();
  static RebuildProvider get instance => _instance;

  rebuild () {
    notifyListeners();
  }
}

class AProvider extends ChangeNotifier {

  AProvider._internal();

  static final AProvider _instance = AProvider._internal();
  static AProvider get instance => _instance;

  MapController mapController = MapController(
    initPosition: GeoPoint(latitude: 0, longitude: 0)
  );

  GeoPoint? _destination;
  GeoPoint? get destination => _destination;

  GeoPoint? _userLocation;
  GeoPoint? get userLocation => _userLocation;

  setDestination(double lat, double long) {
    _destination = GeoPoint(latitude: lat, longitude: long);
    notifyListeners();
  }

  setUserLocation(double lat, double long) {
    _userLocation = GeoPoint(latitude: lat, longitude: long);
  }

  bool _mapLoaded = false;
  bool get mapLoaded => _mapLoaded;

  setMapStatus(bool status) {
    _mapLoaded = status;
    notifyListeners();
  }

  bool _loadingData = true;
  bool get loadingData => _loadingData;

  Future<void> loadData(double lat, double long) async {
    _loadingData = true;
      notifyListeners();

    _userLocation =  GeoPoint(latitude: lat, longitude: long);

    // SOME REQUEST
    var data = {"data": ""} as dynamic;

    _destination = GeoPoint(latitude: data.data![0].coordinates.lat, longitude: data.data[0].coordinates.long);

    _loadingData = false;
    notifyListeners();
    RebuildProvider.instance.rebuild();
  }

  setNewPointDestination(double lat, double long) async {

    if (destination?.latitude == lat && destination?.longitude == long) {
      return;
    }

    mapController.removeLastRoad();
    mapController.removeMarkers([
      userLocation!, destination!
    ]);

    _destination = GeoPoint(latitude: lat, longitude: long);

    _setMarker(userLocation!, Icons.person_pin_circle, Colors.blue, 20);

    mapController.drawRoad(
      userLocation!,
      GeoPoint(latitude: lat, longitude: long),
      roadType: RoadType.foot,
      roadOption: const RoadOption(
        roadWidth: 10,
        roadColor: Colors.blue,
        zoomInto: true,
      ),
    );

    _setMarker(GeoPoint(latitude: lat, longitude: long), Icons.place, Colors.red, 48);
     notifyListeners();
    }


  _setMarker(GeoPoint point, IconData icon, Color? color, double width) {
    mapController.addMarker(
        point,
        markerIcon:MarkerIcon(
          icon: Icon(
            icon,
            color: color,
            size: width,
          ),
        )
    );
  }

  void reset() {
    _destination = null;
    _userLocation = null;

    if (!mapLoaded) {
      mapController = MapController(
        initPosition: GeoPoint(latitude: 0, longitude: 0)
      );
    } else {
      mapController.removeLastRoad();
    }

    _mapLoaded = false;

    notifyListeners();
  }
}


class StreetMap extends StatefulWidget {

  final AProvider _aProvider
   = AProvider.instance;

  final GeoPoint destination;
  StreetMap({Key? key, required this.destination}) : super(key: key);

  @override
  State<StreetMap> createState() => _StreetMapState();
}

class _StreetMapState extends State<StreetMap> {

@override
  void initState() {
    super.initState();
    widget._aProvider.mapController = MapController(
      initPosition: GeoPoint(latitude: 47.4358055, longitude: 8.4737324),
  );
}

  _setMarker(GeoPoint point, IconData icon, Color? color, double width) {
     widget._aProvider.mapController.addMarker(
        point,
        markerIcon:MarkerIcon(
          icon: Icon(
            icon,
            color: color,
            size: width,
          ),
        )
    );
  }

  _setDestination(double lat, double long) {

     widget._aProvider.setUserLocation(lat, long);

     _setMarker(GeoPoint(latitude: lat, longitude: long), Icons.person_pin_circle, Colors.blue, 40);

     widget._aProvider.mapController.drawRoad(
      GeoPoint(latitude: lat, longitude: long),
      widget.destination,
      roadType: RoadType.foot,
      roadOption: const RoadOption(
        roadWidth: 10,
        roadColor: Colors.blue,
        zoomInto: true,
      ),
    );

    _setMarker(widget.destination, Icons.place, Colors.red, 40);

  }


  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;

    return Container(
      height: size.height * .5,
      margin: const EdgeInsets.only(left: 25, right: 25),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(5),
        border: Border.all(
          width: 2,
          color: Colors.blueGrey
        ),
      ),
      child: OSMFlutter(
          userTrackingOption: const UserTrackingOption(
            enableTracking: true,
            unFollowUser: true,
          ),
          controller:  widget._aProvider.mapController,
          enableRotationByGesture: true,
          initZoom: 12,
          minZoomLevel: 9,
          stepZoom: 1.0,
          mapIsLoading: const Text("Map Loading"),
          onMapIsReady: (isMapReady) => {
            if (isMapReady) widget._aProvider.setMapStatus(isMapReady)
          },
          onLocationChanged: (geoPosition) => {
            _setDestination(geoPosition.latitude, geoPosition.longitude)
          },
          roadConfiguration: const RoadOption(
            roadColor: Colors.lightBlue,
          ),
          markerOption: MarkerOption(
            defaultMarker: const MarkerIcon(
              icon: Icon(
                Icons.person_pin_circle,
                  color: Colors.blue,
                  size: 56,
              ),
            )
          ),
      ),
    );
  }
}

class RouteExampleA extends StatelessWidget {
  const RouteExampleA({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        StreetMap(destination: AProvider._instance.destination!),
        TextButton(
          onPressed: () {
           Navigator.push<void>(
            context,
            MaterialPageRoute<void>(
              builder: (BuildContext context) => const RouteExampleB(),
            ),
          );
          }, 
          child: const Text("redirect to class B")
        )
      ],
    );
  }
}

class RouteExampleB extends StatelessWidget {
  const RouteExampleB({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
   return Column(
      children: [
        ListenableBuilder(
          listenable: RebuildProvider._instance,
          builder: (context, child) {
            return StreetMap(destination: AProvider._instance.destination!);
          }
        ),
        TextButton(
          onPressed: () {
           Navigator.push<void>(
            context,
            MaterialPageRoute<void>(
              builder: (BuildContext context) => const RouteExampleA(),
            ),
          );
          }, 
          child: const Text("redirect to class A")
        )
      ],
    );
  }
}



For example, imagine a Route B that redirect to Route A (that route call OSMFlutter), the load of map and destination work fine. Now you click on button that redirect back to Route B > and in the Route B you redirect Back to Route A, now, the map starts to get wrong, the load never stop and map never be completed loaded. To solve this problem, what i did was before redirecting again to the Route A, include a call to reset the map controller, as example:

class RouteExampleB extends StatelessWidget {
  const RouteExampleB({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () {

        AProvider._instance.reset();


        Navigator.push<void>(
        context,
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const RouteExampleA(),
        ),
      );
      }, 
      child: const Text("redirect to class A")
    );
  }
}

gabrielmarinhoindico avatar Jan 12 '24 14:01 gabrielmarinhoindico

you cannot use latest version of flutter and our latest dev version ?

liodali avatar Jan 12 '24 16:01 liodali

why did you try to use navigator in materialApp directly why to try to add MaterialRoutePage like that ?

liodali avatar Jan 12 '24 16:01 liodali

why did you try to use navigator in materialApp directly why to try to add MaterialRoutePage like that?

Was just an example to represent route change and illustrate the case. You can focus on the part of the case that I reported, that made this controller reset solve my issue, I hope that works with the other problems reported above...

you cannot use latest version of flutter and our latest dev version ?

I can't at the moment, maybe later if I can I'll post it here again.

gabrielmarinhoindico avatar Jan 12 '24 17:01 gabrielmarinhoindico