I'm always excited to take on new projects and collaborate with innovative minds.

Phone

+977 9846930428

Email

mr.kashyapsandesh@gmail.com

Website

https://sandeshghimire.info.np

Address

Butwal,Nepal

Social Links

Real-Time Rider Movement in Mobile Apps (No Flickering)

Real-Time Rider Movement in Mobile Apps (No Flickering)

Real-Time Rider Movement in Mobile Apps (No Flickering)

Building a ride-hailing or delivery app? One of the toughest challenges is implementing smooth, real-time rider tracking without the dreaded "flickering marker" problem. This comprehensive guide shows you how to create buttery-smooth marker animations across React Native, Flutter, iOS, and Android.

Table of Contents

  1. Why Markers Flicker (The Wrong Approach)
  2. The Solution: WebSockets + Animation
  3. React Native Implementation
  4. Flutter Implementation
  5. iOS Implementation
  6. Android Implementation
  7. Best Practices & Optimization
  8. Troubleshooting

<a name="why-flicker"></a>

Why Markers Flicker (The Wrong Approach)

Most developers start with an approach like this:

// ❌ WRONG: Polling that causes flickering
setInterval(async () => {
    const riders = await fetchNearbyRiders();
    setMarkers(riders); // Recreates ALL markers
}, 5000);

Problems:

  • Entire marker set recreated every 5 seconds
  • Visible flicker as markers are removed and re-added
  • No smooth transitions between positions
  • Wastes bandwidth fetching unchanged data
  • High server load with many concurrent users

<a name="solution"></a>

The Solution: WebSockets + Animation

Strategy: Initial Load → Real-Time Updates → Smooth Animations

  1. Initial Load: Fetch nearby riders once via REST API
  2. WebSocket Updates: Listen to LocationUpdated events for individual changes
  3. Targeted Updates: Update only the specific marker that moved
  4. Smooth Animation: Interpolate between old and new positions with easing

This approach eliminates flickering and creates smooth, professional marker movement.


<a name="react-native"></a>

React Native Implementation

Setup

npm install laravel-echo pusher-js react-native-maps

Complete Implementation

// RiderMap.js
import React, { useEffect, useRef, useState } from "react";
import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps";
import Echo from "laravel-echo";
import Pusher from "pusher-js";

// Configure Echo
const echo = new Echo({
    broadcaster: "pusher",
    key: process.env.REVERB_APP_KEY,
    wsHost: process.env.REVERB_HOST,
    wsPort: 8080,
    wssPort: 443,
    forceTLS: false,
    enabledTransports: ["ws", "wss"],
    authEndpoint: "https://your-api.com/broadcasting/auth",
    auth: {
        headers: {
            Authorization: `Bearer ${token}`,
        },
    },
});

const RiderMap = ({ initialLocation, token }) => {
    const [riders, setRiders] = useState({});
    const markersRef = useRef({});

    // STEP 1: Load initial riders once
    useEffect(() => {
        fetchInitialRiders();
    }, []);

    const fetchInitialRiders = async () => {
        const response = await fetch("/api/riders/nearby", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${token}`,
            },
            body: JSON.stringify({
                latitude: initialLocation.latitude,
                longitude: initialLocation.longitude,
                radius: 1,
            }),
        });

        const { data } = await response.json();
        
        const ridersMap = {};
        data.riders.forEach((rider) => {
            ridersMap[rider.id] = {
                latitude: parseFloat(rider.latitude),
                longitude: parseFloat(rider.longitude),
                ...rider,
            };
        });

        setRiders(ridersMap);
    };

    // STEP 2: Listen to real-time updates
    useEffect(() => {
        const channel = echo.channel("rider-locations");

        channel.listen(".location.updated", (event) => {
            const { rider_id, location } = event;
            
            updateSingleRider(rider_id, {
                latitude: parseFloat(location.latitude),
                longitude: parseFloat(location.longitude),
                ...location,
            });
        });

        return () => echo.leave("rider-locations");
    }, []);

    // STEP 3: Update only the specific rider
    const updateSingleRider = (riderId, newLocation) => {
        setRiders((prev) => {
            const existing = prev[riderId];

            if (!existing) {
                return { ...prev, [riderId]: newLocation };
            }

            // Animate the marker smoothly
            const marker = markersRef.current[riderId];
            if (marker) {
                marker.animateMarkerToCoordinate(
                    {
                        latitude: newLocation.latitude,
                        longitude: newLocation.longitude,
                    },
                    1000 // 1 second animation
                );
            }

            return {
                ...prev,
                [riderId]: { ...existing, ...newLocation },
            };
        });
    };

    return (
        <MapView
            provider={PROVIDER_GOOGLE}
            style={{ flex: 1 }}
            initialRegion={{
                ...initialLocation,
                latitudeDelta: 0.01,
                longitudeDelta: 0.01,
            }}
        >
            {Object.entries(riders).map(([id, rider]) => (
                <Marker
                    key={id}
                    ref={(ref) => {
                        if (ref) markersRef.current[id] = ref;
                    }}
                    coordinate={{
                        latitude: rider.latitude,
                        longitude: rider.longitude,
                    }}
                    title={rider.name}
                />
            ))}
        </MapView>
    );
};

export default RiderMap;

<a name="flutter"></a>

Flutter Implementation

Setup

# pubspec.yaml
dependencies:
  google_maps_flutter: ^2.2.0
  pusher_channels_flutter: ^2.0.0
  http: ^1.1.0

Complete Implementation

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:pusher_channels_flutter/pusher_channels_flutter.dart';

class RiderMap extends StatefulWidget {
  final LatLng initialLocation;
  final String token;

  const RiderMap({required this.initialLocation, required this.token});

  @override
  State<RiderMap> createState() => _RiderMapState();
}

class _RiderMapState extends State<RiderMap> {
  GoogleMapController? _controller;
  final Map<String, Marker> _markers = {};
  final Map<String, Timer> _animations = {};
  PusherChannelsFlutter pusher = PusherChannelsFlutter.getInstance();

  @override
  void initState() {
    super.initState();
    _initPusher();
    _loadRiders();
  }

  // STEP 1: Initialize WebSocket connection
  Future<void> _initPusher() async {
    await pusher.init(
      apiKey: 'YOUR_KEY',
      cluster: '',
      authEndpoint: 'https://your-api.com/broadcasting/auth',
      onEvent: _handleLocationUpdate,
    );

    await pusher.subscribe(channelName: 'rider-locations');
    await pusher.connect();
  }

  // STEP 2: Load initial riders
  Future<void> _loadRiders() async {
    final response = await http.post(
      Uri.parse('https://your-api.com/api/riders/nearby'),
      headers: {'Authorization': 'Bearer ${widget.token}'},
      body: jsonEncode({
        'latitude': widget.initialLocation.latitude,
        'longitude': widget.initialLocation.longitude,
      }),
    );

    final data = jsonDecode(response.body);
    
    setState(() {
      for (var rider in data['riders']) {
        _markers[rider['id']] = Marker(
          markerId: MarkerId(rider['id']),
          position: LatLng(
            double.parse(rider['latitude']),
            double.parse(rider['longitude']),
          ),
          infoWindow: InfoWindow(title: rider['name']),
        );
      }
    });
  }

  // STEP 3: Handle real-time updates
  void _handleLocationUpdate(PusherEvent event) {
    if (event.eventName != '.location.updated') return;

    final data = jsonDecode(event.data);
    final riderId = data['rider_id'];
    final location = data['location'];
    
    final newPos = LatLng(
      double.parse(location['latitude']),
      double.parse(location['longitude']),
    );

    _animateMarker(riderId, newPos);
  }

  // STEP 4: Animate marker smoothly
  void _animateMarker(String riderId, LatLng target) {
    _animations[riderId]?.cancel();

    final current = _markers[riderId]?.position;
    if (current == null) return;

    const duration = Duration(seconds: 1);
    const steps = 30;
    final stepDuration = Duration(milliseconds: 1000 ~/ steps);

    int step = 0;

    _animations[riderId] = Timer.periodic(stepDuration, (timer) {
      step++;
      final progress = step / steps;
      final eased = 1 - pow(1 - progress, 3); // Ease-out cubic

      final lat = current.latitude + 
          (target.latitude - current.latitude) * eased;
      final lng = current.longitude + 
          (target.longitude - current.longitude) * eased;

      setState(() {
        _markers[riderId] = _markers[riderId]!.copyWith(
          positionParam: LatLng(lat, lng),
        );
      });

      if (step >= steps) {
        timer.cancel();
        _animations.remove(riderId);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return GoogleMap(
      initialCameraPosition: CameraPosition(
        target: widget.initialLocation,
        zoom: 14,
      ),
      onMapCreated: (controller) => _controller = controller,
      markers: Set.from(_markers.values),
    );
  }

  @override
  void dispose() {
    _animations.forEach((_, timer) => timer.cancel());
    pusher.disconnect();
    super.dispose();
  }
}

<a name="ios"></a>

iOS (Swift) Implementation

Setup

# Podfile
pod 'PusherSwift'

Complete Implementation

import UIKit
import MapKit
import PusherSwift

class RiderMapViewController: UIViewController {
    @IBOutlet weak var mapView: MKMapView!
    
    private var riders: [String: RiderAnnotation] = [:]
    private var pusher: Pusher!
    private var animationTimers: [String: Timer] = [:]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupPusher()
        loadInitialRiders()
    }
    
    // STEP 1: Setup WebSocket
    func setupPusher() {
        let options = PusherClientOptions(
            authMethod: .endpoint(
                authEndpoint: "https://your-api.com/broadcasting/auth"
            )
        )
        
        pusher = Pusher(key: "YOUR_KEY", options: options)
        let channel = pusher.subscribe("rider-locations")
        
        channel.bind(eventName: ".location.updated") { [weak self] event in
            self?.handleLocationUpdate(event)
        }
        
        pusher.connect()
    }
    
    // STEP 2: Load initial riders
    func loadInitialRiders() {
        // Make API call and add annotations
        // ... (API call code)
    }
    
    // STEP 3: Handle location update
    func handleLocationUpdate(_ event: PusherEvent) {
        guard let data = event.data?.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let riderId = json["rider_id"] as? String,
              let location = json["location"] as? [String: Any],
              let lat = Double(location["latitude"] as? String ?? ""),
              let lng = Double(location["longitude"] as? String ?? "") else {
            return
        }
        
        let newCoord = CLLocationCoordinate2D(latitude: lat, longitude: lng)
        animateMarker(riderId: riderId, to: newCoord)
    }
    
    // STEP 4: Animate marker
    func animateMarker(riderId: String, to target: CLLocationCoordinate2D) {
        animationTimers[riderId]?.invalidate()
        
        guard let annotation = riders[riderId] else { return }
        let from = annotation.coordinate
        
        let duration: TimeInterval = 1.0
        let steps = 30
        var step = 0
        
        let timer = Timer.scheduledTimer(withTimeInterval: duration / Double(steps), repeats: true) { [weak self] timer in
            step += 1
            let progress = Double(step) / Double(steps)
            let eased = 1 - pow(1 - progress, 3)
            
            let lat = from.latitude + (target.latitude - from.latitude) * eased
            let lng = from.longitude + (target.longitude - from.longitude) * eased
            
            annotation.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
            
            if step >= steps {
                timer.invalidate()
                self?.animationTimers.removeValue(forKey: riderId)
            }
        }
        
        animationTimers[riderId] = timer
    }
}

class RiderAnnotation: NSObject, MKAnnotation {
    @objc dynamic var coordinate: CLLocationCoordinate2D
    var title: String?
    let riderId: String
    
    init(riderId: String, coordinate: CLLocationCoordinate2D, title: String?) {
        self.riderId = riderId
        self.coordinate = coordinate
        self.title = title
    }
}

<a name="android"></a>

Android (Kotlin) Implementation

Setup

// build.gradle
dependencies {
    implementation("com.pusher:pusher-java-client:2.4.0")
    implementation("com.google.android.gms:play-services-maps:18.2.0")
}

Complete Implementation

import com.google.android.gms.maps.*
import com.google.android.gms.maps.model.*
import com.pusher.client.Pusher
import com.pusher.client.PusherOptions
import org.json.JSONObject
import android.os.Handler
import android.os.Looper

class RiderMapActivity : AppCompatActivity(), OnMapReadyCallback {
    private lateinit var map: GoogleMap
    private val markers = mutableMapOf<String, Marker>()
    private val animations = mutableMapOf<String, Handler>()
    private lateinit var pusher: Pusher
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)
        
        setupMap()
        setupPusher()
        loadInitialRiders()
    }
    
    private fun setupMap() {
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }
    
    // STEP 1: Setup WebSocket
    private fun setupPusher() {
        val options = PusherOptions().apply {
            setHost("your-host")
            setWsPort(8080)
        }
        
        pusher = Pusher("YOUR_KEY", options)
        val channel = pusher.subscribe("rider-locations")
        
        channel.bind(".location.updated") { event ->
            handleLocationUpdate(event.data)
        }
        
        pusher.connect()
    }
    
    // STEP 2: Load initial riders
    private fun loadInitialRiders() {
        // Make API call and add markers
        // ... (API call code)
    }
    
    // STEP 3: Handle location update
    private fun handleLocationUpdate(data: String) {
        val json = JSONObject(data)
        val riderId = json.getString("rider_id")
        val location = json.getJSONObject("location")
        
        val newPos = LatLng(
            location.getString("latitude").toDouble(),
            location.getString("longitude").toDouble()
        )
        
        runOnUiThread {
            animateMarker(riderId, newPos)
        }
    }
    
    // STEP 4: Animate marker
    private fun animateMarker(riderId: String, target: LatLng) {
        val marker = markers[riderId] ?: return
        animations[riderId]?.removeCallbacksAndMessages(null)
        
        val start = marker.position
        val duration = 1000L
        val steps = 30
        var step = 0
        
        val handler = Handler(Looper.getMainLooper())
        val runnable = object : Runnable {
            override fun run() {
                step++
                val progress = step.toFloat() / steps
                val eased = 1 - Math.pow((1 - progress).toDouble(), 3.0).toFloat()
                
                val lat = start.latitude + (target.latitude - start.latitude) * eased
                val lng = start.longitude + (target.longitude - start.longitude) * eased
                
                marker.position = LatLng(lat, lng)
                
                if (step < steps) {
                    handler.postDelayed(this, duration / steps)
                } else {
                    animations.remove(riderId)
                }
            }
        }
        
        animations[riderId] = handler
        handler.post(runnable)
    }
    
    override fun onMapReady(googleMap: GoogleMap) {
        map = googleMap
    }
    
    override fun onDestroy() {
        super.onDestroy()
        pusher.disconnect()
    }
}

<a name="best-practices"></a>

Best Practices & Optimization

1. Use Proper Easing Functions

// Ease-out cubic (smooth deceleration)
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);

// Ease-in-out (smooth start and end)
const easeInOutCubic = (t) => 
    t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

2. Throttle Insignificant Updates

const MIN_DISTANCE = 0.0001; // ~11 meters

if (calculateDistance(old, new) < MIN_DISTANCE) {
    return; // Ignore tiny movements
}

3. Handle Connection Failures

pusher.connection.bind('disconnected', () => {
    // Fall back to polling
    startPollingFallback();
});

pusher.connection.bind('connected', () => {
    stopPollingFallback();
});

4. Optimize for Many Riders

  • Use marker clustering for 100+ riders
  • Only render markers in viewport
  • Implement virtual scrolling for lists
  • Debounce rapid updates

5. Memory Management

// Always clean up
useEffect(() => {
    return () => {
        // Cancel animations
        Object.values(animationRefs.current).forEach(cancel);
        // Leave channels
        echo.leave('rider-locations');
    };
}, []);

<a name="troubleshooting"></a>

Troubleshooting Common Issues

Markers Still Flicker

Cause: You're still recreating markers instead of updating positions.

Fix: Store marker references and update their coordinates directly:

// ❌ Wrong
setMarkers([...newMarkers]); // Recreates all

// ✅ Right
marker.animateMarkerToCoordinate(newPosition);

Choppy Animation

Cause: Too few animation steps or blocking the main thread.

Fix: Use 30-60 steps and requestAnimationFrame:

const animate = () => {
    // Update position
    if (progress < 1) {
        requestAnimationFrame(animate);
    }
};

WebSocket Disconnects

Cause: Mobile networks are unstable.

Fix: Implement reconnection logic with exponential backoff:

let retryCount = 0;

pusher.connection.bind('disconnected', () => {
    const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
    setTimeout(() => pusher.connect(), delay);
    retryCount++;
});

High Battery Drain

Cause: Too many location updates or animations.

Fix:

  • Reduce update frequency to 5-10 seconds
  • Use location-based throttling
  • Pause updates when app is backgrounded

Conclusion

Implementing smooth, flicker-free rider movement requires three key ingredients:

  1. Initial REST API call for nearby riders
  2. WebSocket updates for individual location changes
  3. Smooth interpolation with easing functions

This approach works across all platforms and creates the polished experience users expect from modern ride-hailing apps.

Key Takeaways

  • Never poll and recreate all markers
  • Update only the changed marker
  • Always animate position changes
  • Handle network failures gracefully
  • Clean up animations and connections

With this implementation, your rider markers will glide smoothly across the map, creating a professional and delightful user experience.

10 min read
Dec 05, 2025
By Sandesh Ghimire
Share

Leave a comment

Your email address will not be published. Required fields are marked *

Your experience on this site will be improved by allowing cookies. Cookie Policy