relay_flutter 1.2.1 copy "relay_flutter: ^1.2.1" to clipboard
relay_flutter: ^1.2.1 copied to clipboard

Official Relay Delivery Platform Mobile SDK - Real-time WebSocket client for Flutter applications (iOS, Android, Web, Desktop)

Relay Flutter SDK #

pub package License: MIT 📱 Mobile SDK | WebSocket Client

Official Flutter SDK for the Relay delivery platform. This is a client-side mobile SDK designed for Flutter applications.

â„šī¸ Client-Side SDK: This SDK uses WebSocket tokens (not API keys) for secure client authentication. Tokens must be provisioned by your backend using @relay-sdk/sdk-node (Server SDK).

Platform Support: iOS â€ĸ Android â€ĸ Web â€ĸ macOS â€ĸ Windows â€ĸ Linux

Features #

  • 🔄 Real-time Task Tracking - WebSocket-based live updates
  • 🔐 Automatic Token Management - Token provisioning and auto-refresh
  • 📡 19 Event Types - Task lifecycle, payments, rider location
  • 🔌 Auto-Reconnection - Exponential backoff with subscription preservation
  • 💓 Heartbeat Monitoring - Connection health checks
  • 📝 Offline Message Queuing - Buffer messages when disconnected
  • đŸŽ¯ Type-Safe Event Streams - Strongly typed Dart streams
  • 🔧 Zero Configuration - Sensible defaults, minimal setup
  • 🌍 Platform Support - Android, iOS, Web, Desktop

Usage Context #

Mobile SDK (Flutter Applications) #

This SDK is designed for client-side Flutter applications and provides:

  • ✅ Real-time WebSocket connection for live task updates
  • ✅ Automatic token provisioning via backend callback
  • ✅ 19+ event types for complete task lifecycle tracking
  • ✅ Auto-reconnection with exponential backoff
  • ✅ Token auto-refresh before expiration
  • ✅ Offline message queuing
  • ✅ Type-safe Dart Streams for all events
  • ✅ Cross-platform support (iOS, Android, Web, Desktop)

Typical Use Cases:

  • Mobile delivery tracking apps (customer-facing)
  • Rider applications (task offers and navigation)
  • Real-time order status updates
  • Live rider location on maps
  • Push notification triggers
  • Cross-platform delivery management

Security Model: This SDK uses temporary WebSocket tokens instead of API keys. Your backend (using @relay-sdk/sdk-node) generates these tokens with limited scope and expiration:

// Frontend (Flutter) - Request token from backend
final relay = RelayRealtimeClient(
  getToken: (taskIds) async {
    final response = await http.post(
      Uri.parse('https://your-backend.com/api/relay/token'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'taskIds': taskIds}),
    );
    return jsonDecode(response.body)['token'];
  },
);
// Backend (Node.js) - Generate token with @relay-sdk/sdk-node
const { token } = await relay.auth.createWebSocketToken({
  scope: ['task:task-123'],
  expiresIn: 3600, // 1 hour
});

Server-Side Alternative #

For server-side task creation and management:

  • Node.js backend: Use @relay-sdk/sdk-node - REST API client with full access

Web Alternative #

For browser-based web applications (non-Flutter):

  • Web apps: Use @relay-sdk/sdk-browser - WebSocket client optimized for browsers

Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  relay_flutter: ^1.0.0

Then run:

flutter pub get

Quick Start #

import 'package:relay_flutter/relay_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

// 1. Create Relay client
final relay = RelayRealtimeClient(
  getToken: (taskIds) async {
    // Fetch token from your backend
    final response = await http.post(
      Uri.parse('https://your-backend.com/api/relay/token'),
      headers: {
        'Authorization': 'Bearer $userToken',
        'Content-Type': 'application/json',
      },
      body: jsonEncode({'taskIds': taskIds}),
    );

    final data = jsonDecode(response.body);
    return data['token'] as String;
  },
);

// 2. Start listening to a task
await relay.listen('task-123');

// 3. Subscribe to events
relay.onTaskAssigned.listen((event) {
  print('Task ${event.taskId} assigned to rider ${event.riderId}');
});

relay.onRiderLocationUpdate.listen((event) {
  print('Rider at ${event.location.latitude}, ${event.location.longitude}');
  updateMapMarker(event.location);
});

relay.onTaskCompleted.listen((event) {
  print('Task completed!');
  showCompletionDialog();
});

// 4. Cleanup when done
@override
void dispose() {
  relay.dispose();
  super.dispose();
}

Backend Token Endpoint #

The SDK requires a backend endpoint to fetch WebSocket tokens. This ensures your API keys never leave your backend.

Example Backend (Node.js/Express) #

import { RelayClient } from '@relay-sdk/sdk-node';

const relay = new RelayClient({
  apiKey: process.env.RELAY_API_KEY, // Server-side API key
});

app.post('/api/relay/token', authenticateUser, async (req, res) => {
  const { taskIds } = req.body;

  // Fetch token from Relay API
  const { token } = await relay.auth.createWebSocketToken({
    scope: taskIds.map((id) => `task:${id}`),
    expiresIn: 3600, // 1 hour
  });

  res.json({ token });
});

Example Backend (Python/FastAPI) #

from relay_sdk import RelayClient
from fastapi import FastAPI, Depends

relay = RelayClient(api_key=os.getenv("RELAY_API_KEY"))

@app.post("/api/relay/token")
async def create_token(task_ids: list[str], user=Depends(get_current_user)):
    token_response = relay.auth.create_websocket_token(
        task_ids=task_ids,
        type="APP",
        expires_in=3600,
    )
    return {"token": token_response.token}

Usage #

Basic Task Tracking #

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

class TaskTrackerScreen extends StatefulWidget {
  final String taskId;
  const TaskTrackerScreen({required this.taskId});

  @override
  State<TaskTrackerScreen> createState() => _TaskTrackerScreenState();
}

class _TaskTrackerScreenState extends State<TaskTrackerScreen> {
  late RelayRealtimeClient _relay;
  String _status = 'PENDING';
  Location? _riderLocation;

  @override
  void initState() {
    super.initState();

    _relay = RelayRealtimeClient(
      getToken: _fetchToken,
    );

    // Start listening to task
    _relay.listen(widget.taskId);

    // Subscribe to events
    _relay.onTaskAssigned.listen((event) {
      if (event.taskId == widget.taskId) {
        setState(() => _status = 'ASSIGNED');
      }
    });

    _relay.onTaskInProgress.listen((event) {
      if (event.taskId == widget.taskId) {
        setState(() => _status = 'IN_PROGRESS');
      }
    });

    _relay.onTaskCompleted.listen((event) {
      if (event.taskId == widget.taskId) {
        setState(() => _status = 'COMPLETED');
        _showCompletionDialog();
      }
    });

    _relay.onRiderLocationUpdate.listen((event) {
      if (event.taskId == widget.taskId) {
        setState(() => _riderLocation = event.location);
      }
    });
  }

  Future<String> _fetchToken(List<String> taskIds) async {
    final response = await http.post(
      Uri.parse('https://your-backend.com/api/relay/token'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'taskIds': taskIds}),
    );
    return jsonDecode(response.body)['token'];
  }

  @override
  void dispose() {
    _relay.stopListening(widget.taskId);
    _relay.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Task ${widget.taskId}')),
      body: Column(
        children: [
          StatusBadge(status: _status),
          if (_riderLocation != null)
            GoogleMap(
              initialCameraPosition: CameraPosition(
                target: LatLng(
                  _riderLocation!.latitude,
                  _riderLocation!.longitude,
                ),
                zoom: 15,
              ),
            ),
        ],
      ),
    );
  }
}

Tracking Multiple Tasks #

class OrderListScreen extends StatefulWidget {
  @override
  State<OrderListScreen> createState() => _OrderListScreenState();
}

class _OrderListScreenState extends State<OrderListScreen> {
  late RelayRealtimeClient _relay;
  final Map<String, String> _taskStatuses = {};
  final List<String> _taskIds = ['task-1', 'task-2', 'task-3'];

  @override
  void initState() {
    super.initState();

    _relay = RelayRealtimeClient(
      getToken: _fetchToken,
      logger: Logger('RelaySDK'),
    );

    // Listen to all tasks
    for (final taskId in _taskIds) {
      _relay.listen(taskId);
    }

    // Update statuses on events
    _relay.onTaskAssigned.listen((event) {
      setState(() => _taskStatuses[event.taskId] = 'ASSIGNED');
    });

    _relay.onTaskInProgress.listen((event) {
      setState(() => _taskStatuses[event.taskId] = 'IN_PROGRESS');
    });

    _relay.onTaskCompleted.listen((event) {
      setState(() => _taskStatuses[event.taskId] = 'COMPLETED');
    });

    _relay.onTaskFailed.listen((event) {
      setState(() => _taskStatuses[event.taskId] = 'FAILED');
      _showErrorDialog(event.reason);
    });
  }

  Future<String> _fetchToken(List<String> taskIds) async {
    // Your token fetch logic
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('My Orders')),
      body: ListView.builder(
        itemCount: _taskIds.length,
        itemBuilder: (context, index) {
          final taskId = _taskIds[index];
          final status = _taskStatuses[taskId] ?? 'PENDING';

          return ListTile(
            title: Text('Order ${index + 1}'),
            subtitle: Text('Status: $status'),
            trailing: _buildStatusIcon(status),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    _relay.dispose();
    super.dispose();
  }
}

Custom Configuration #

final relay = RelayRealtimeClient(
  getToken: _fetchToken,
  url: 'wss://custom-relay-ws.com/v1',
  deviceId: 'my-device-123',
  autoRefresh: true,
  tokenExpirationWarning: Duration(minutes: 5),
  reconnect: ReconnectConfig(
    enabled: true,
    maxAttempts: 10,
    initialDelay: Duration(seconds: 1),
    maxDelay: Duration(seconds: 30),
    backoffMultiplier: 1.5,
  ),
  heartbeat: HeartbeatConfig(
    enabled: true,
    interval: Duration(seconds: 30),
    timeout: Duration(seconds: 10),
  ),
  logger: Logger('RelaySDK')..level = Level.ALL,
);

Listening to All Events #

// Listen to all events with type checking
relay.events.listen((event) {
  if (event is TaskAssignedEvent) {
    print('Task assigned: ${event.taskId}');
  } else if (event is TaskCompletedEvent) {
    print('Task completed: ${event.taskId}');
  } else if (event is RiderLocationUpdateEvent) {
    updateMap(event.location);
  } else if (event is ConnectionErrorEvent) {
    showErrorSnackbar(event.error);
  }
});

Connection Monitoring #

relay.onConnectionOpen.listen((_) {
  print('Connected to Relay WebSocket');
  setState(() => _connectionStatus = 'Connected');
});

relay.onConnectionClose.listen((event) {
  print('Disconnected: ${event.reason}');
  setState(() => _connectionStatus = 'Disconnected');
});

relay.onConnectionReconnecting.listen((event) {
  print('Reconnecting (attempt ${event.attempt} in ${event.delay}ms)...');
  setState(() => _connectionStatus = 'Reconnecting...');
});

relay.onTokenExpiring.listen((event) {
  print('Token expiring at ${event.expiresAt}');
  // Token will auto-refresh if autoRefresh is enabled
});

Manual Connection Control #

// Manually connect (usually not needed - listen() auto-connects)
await relay.connect();

// Check connection state
if (relay.isConnected()) {
  print('WebSocket is connected');
}

final state = relay.getState();
// ConnectionState.connecting | connected | disconnecting | disconnected | reconnecting

// Manually disconnect
relay.disconnect();

Event Types #

Task Lifecycle Events #

Event Description
TaskCreatedEvent New task created (webhook only)
TaskOfferedEvent Task offered to riders
TaskAssignedEvent Task assigned to specific rider
TaskInProgressEvent Rider started task
TaskCompletedEvent Task completed successfully
TaskFailedEvent Task failed (timeout, rider issue)
TaskCancelledEvent Task cancelled by developer
StageCompletedEvent Task stage (pickup/dropoff) completed

Payment Events #

Event Description
PaymentPendingEvent Payment held in escrow (24h)
PaymentReleasedEvent Payment released to rider
PaymentDisputedEvent Payment disputed by developer
PaymentDisputeResolvedEvent Dispute resolved by admin

Real-time Events #

Event Description
RiderLocationUpdateEvent Rider GPS location update
NewTaskOfferEvent New task offer (rider-side only)

Connection Events #

Event Description
ConnectionOpenEvent WebSocket connected
ConnectionCloseEvent WebSocket disconnected
ConnectionErrorEvent Connection error occurred
ConnectionReconnectingEvent Reconnection in progress
TokenExpiringEvent Token about to expire

API Reference #

RelayRealtimeClient #

Constructor

RelayRealtimeClient({
  required Future<String> Function(List<String> taskIds) getToken,
  String? url,
  String? deviceId,
  bool autoRefresh = true,
  Duration tokenExpirationWarning = const Duration(minutes: 5),
  ReconnectConfig? reconnect,
  HeartbeatConfig? heartbeat,
  Logger? logger,
})

Notes:

  • url defaults to wss://ws.sendrelay.com.ng/v1
  • deviceId is optional. If omitted, the SDK generates and persists one locally.

Methods

Method Description
listen(String taskId) Start listening to task events
stopListening(String taskId) Stop listening to task events
getListeningTo() Get list of task IDs being tracked
connect() Manually connect to WebSocket
disconnect({bool graceful}) Disconnect from WebSocket
getState() Get current connection state
isConnected() Check if connected
getSubscriptions() Get all active subscriptions
clearSubscriptions() Clear all subscriptions
dispose() Cleanup resources (call in dispose())

Event Streams

Stream Event Type
events All events
onTaskCreated TaskCreatedEvent
onTaskOffered TaskOfferedEvent
onTaskAssigned TaskAssignedEvent
onTaskInProgress TaskInProgressEvent
onTaskCompleted TaskCompletedEvent
onTaskFailed TaskFailedEvent
onTaskCancelled TaskCancelledEvent
onStageCompleted StageCompletedEvent
onPaymentPending PaymentPendingEvent
onPaymentReleased PaymentReleasedEvent
onPaymentDisputed PaymentDisputedEvent
onPaymentDisputeResolved PaymentDisputeResolvedEvent
onRiderLocationUpdate RiderLocationUpdateEvent
onNewTaskOffer NewTaskOfferEvent
onConnectionOpen ConnectionOpenEvent
onConnectionClose ConnectionCloseEvent
onConnectionError ConnectionErrorEvent
onConnectionReconnecting ConnectionReconnectingEvent
onTokenExpiring TokenExpiringEvent

Configuration #

ReconnectConfig #

ReconnectConfig({
  bool enabled = true,
  int maxAttempts = -1,        // -1 = infinite
  Duration initialDelay = Duration(seconds: 1),
  Duration maxDelay = Duration(seconds: 30),
  double backoffMultiplier = 1.5,
})

HeartbeatConfig #

HeartbeatConfig({
  bool enabled = true,
  Duration interval = Duration(seconds: 30),
  Duration timeout = Duration(seconds: 10),
})

Error Handling #

try {
  await relay.listen('task-123');
} on ConnectionError catch (e) {
  print('Connection failed: ${e.message}');
  showErrorDialog('Unable to connect to Relay');
} on TokenError catch (e) {
  print('Token error: ${e.message}');
  refreshAuthToken();
} on RelayError catch (e) {
  print('Relay error: ${e.message}');
}

// Or listen to error events
relay.onConnectionError.listen((event) {
  print('Connection error: ${event.error}');
  showErrorSnackbar(event.error);
});

Testing #

For unit tests, you can mock the getToken callback:

final relay = RelayRealtimeClient(
  getToken: (taskIds) async {
    return 'mock-jwt-token';
  },
  url: 'wss://localhost:8080', // Mock WebSocket server
);

Platform Support #

Platform Status
Android ✅ Supported
iOS ✅ Supported
Web ✅ Supported
macOS ✅ Supported
Windows ✅ Supported
Linux ✅ Supported

Migration from Browser SDK #

If you're familiar with the Browser SDK (@relay-sdk/sdk-browser), the Flutter SDK maintains API parity:

Browser SDK Flutter SDK
new RelayRealtimeClient() RelayRealtimeClient()
client.listen('task-123') await relay.listen('task-123')
client.on('TASK_ASSIGNED', handler) relay.onTaskAssigned.listen(handler)
client.stopListening('task-123') await relay.stopListening('task-123')
client.disconnect() relay.disconnect()

The main difference is using Dart Stream<T> instead of event emitters.

Resources #

License #

MIT License - see LICENSE file for details.

Support #

For questions and support:

1
likes
155
points
433
downloads

Documentation

API reference

Publisher

verified publishercolman.tk

Weekly Downloads

Official Relay Delivery Platform Mobile SDK - Real-time WebSocket client for Flutter applications (iOS, Android, Web, Desktop)

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

flutter, logging, meta, shared_preferences, web_socket_channel

More

Packages that depend on relay_flutter