Skip to content

Why does Flutter rendering behave differently on the same device across various iOS versions? #168994

Closed
@jesussmile

Description

@jesussmile

Steps to reproduce

Hey, my Flutter map with custom hillshade and elevation layers is buttery smooth on iOS 14, but iOS 17 and up is super jerky and flickery. Why's iOS 14 so much better, and what's changed in Flutter and animation to cause this? The code's the same, and empeller's enabled on both. The ipad model is the same with different IOS version running. The ipad above is running ios 14 and below its ios 17

The video is attached below
https://www.youtube.com/shorts/uYCd1qMzeEE

Code sample

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

void main() {
  runApp(const MaterialApp(home: TerrainFlickerDemo()));
}

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

  @override
  State<TerrainFlickerDemo> createState() => _TerrainFlickerDemoState();
}

class _TerrainFlickerDemoState extends State<TerrainFlickerDemo> {
  final ValueNotifier<double> _referenceAltitude = ValueNotifier<double>(6000.0);
  final ValueNotifier<double> _terrainResolution = ValueNotifier<double>(100.0);
  late MapController mapController;
  Timer? _terrainUpdateTimer;
  bool _isSliderDragging = false;
  double _lastAppliedAltitude = 0.0;
  double _currentZoomLevel = 4.0;
  
  // Get bucket size for altitude changes
  double get _bucketSizeFeet => math.max(_terrainResolution.value, 100.0);

  @override
  void initState() {
    super.initState();
    mapController = MapController();
    
    // Monitor zoom changes
    mapController.mapEventStream.listen((event) {
      if (event is MapEventMove) {
        _currentZoomLevel = event.camera.zoom;
      }
    });
  }

  // Schedule terrain updates with zoom-based delays
  void _scheduleTerrainUpdate(double targetAltitude, {bool highZoomDelay = false}) {
    _terrainUpdateTimer?.cancel();

    // Use longer delays for high zoom levels
    int delay = highZoomDelay 
        ? (_currentZoomLevel > 10 ? 300 : _currentZoomLevel > 8 ? 200 : 150)
        : 100;

    _terrainUpdateTimer = Timer(Duration(milliseconds: delay), () {
      final bucketedValue = (targetAltitude / _bucketSizeFeet).round() * _bucketSizeFeet;
      if (bucketedValue != _lastAppliedAltitude) {
        _lastAppliedAltitude = bucketedValue;
        _referenceAltitude.value = bucketedValue;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          FlutterMap(
            mapController: mapController,
            options: MapOptions(
              initialCenter: const latLng.LatLng(39.8, -98.5),
              initialZoom: 4.0,
              maxZoom: 17,
              minZoom: 3,
            ),
            children: [
              // Terrain layer implementation causing flickering
              Opacity(
                opacity: 0.6,
                child: ValueListenableBuilder<double>(
                  valueListenable: _referenceAltitude,
                  builder: (context, altitude, _) {
                    final bucketedValue = (_referenceAltitude.value / _bucketSizeFeet).round();
                    
                    return AnimatedSwitcher(
                      duration: const Duration(milliseconds: 10),
                      transitionBuilder: (child, animation) {
                        return FadeTransition(opacity: animation, child: child);
                      },
                      child: RepaintBoundary(
                        key: ValueKey('terrain_$bucketedValue'),
                        child: LercTileLayer(
                          assetPath: 'assets/elevation.lerc2',
                          referenceAltitude: _referenceAltitude,
                          terrainResolution: _terrainResolution,
                        ),
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
          
          // Slider control that triggers flickering
          Positioned(
            bottom: 16,
            left: 16,
            right: 16,
            child: Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    ValueListenableBuilder<double>(
                      valueListenable: _referenceAltitude,
                      builder: (context, value, _) {
                        return Text(
                          'Altitude: ${value.toInt()}ft',
                          style: const TextStyle(fontSize: 16),
                        );
                      },
                    ),
                    const SizedBox(height: 8),
                    GestureDetector(
                      onHorizontalDragStart: (_) => _isSliderDragging = true,
                      onHorizontalDragEnd: (_) {
                        _isSliderDragging = false;
                        _scheduleTerrainUpdate(_referenceAltitude.value);
                      },
                      child: ValueListenableBuilder<double>(
                        valueListenable: _referenceAltitude,
                        builder: (context, value, _) {
                          return Slider(
                            value: value,
                            min: 0,
                            max: 30000,
                            divisions: (30000 / _terrainResolution.value).round(),
                            label: '${value.round()}ft',
                            onChanged: (newValue) {
                              if (_currentZoomLevel > 7 && _isSliderDragging) {
                                // Skip updates during high zoom dragging
                                _referenceAltitude.value = newValue;
                              } else {
                                _scheduleTerrainUpdate(
                                  newValue,
                                  highZoomDelay: _currentZoomLevel > 7,
                                );
                              }
                            },
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _referenceAltitude.dispose();
    _terrainResolution.dispose();
    mapController.dispose();
    _terrainUpdateTimer?.cancel();
    super.dispose();
  }
}

Performance profiling on master channel

  • The issue still persists on the master channel

Timeline Traces

Timeline Traces JSON
[Paste the Timeline Traces here]

Video demonstration

https://youtube.com/shorts/uYCd1qMzeEE?feature=shared

https://www.youtube.com/shorts/uYCd1qMzeEE

What target platforms are you seeing this bug on?

iOS

OS/Browser name and version | Device information

ipad pro 10.5 ios 14
ipad pro 10.5 ios 17

Does the problem occur on emulator/simulator as well as on physical devices?

Unknown

Is the problem only reproducible with Impeller?

N/A

Logs

No response

Flutter Doctor output

Doctor output
(base) pannam@MacBookPro flightcanvas_terrain % flutter doctor      
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.29.0, on macOS 15.2 24C101 darwin-x64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.3)
[✓] VS Code (version 1.100.0)
[✓] VS Code (version 1.99.0-insider)
[✓] Connected device (5 available)
[✓] Network resources

• No issues found!

Metadata

Metadata

Assignees

No one assigned

    Labels

    in triagePresently being triaged by the triage teamwaiting for customer responseThe Flutter team cannot make further progress on this issue until the original reporter responds

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions