← Use cases

Flutter on iOS, Android and the web

One Dart client: private channels, reconnects, and push when the app is closed.

Flutter builds one app for iOS, Android, the web and desktop, and its realtime code should be one piece too. The official Pusher Flutter plugin always connects to pusher.com hosts, so use dart_pusher_channels: a pure Dart client for the Pusher protocol that lets you set the host and runs everywhere Flutter does.

What your users get

  • Order, chat and score screens that update in place, without pull-to-refresh.
  • A connection that comes back by itself after a tunnel, a lift or a switch from Wi-Fi to mobile data.
  • The same behaviour on a phone and in the browser.

Built with

  • dart_pusher_channels, one client for the whole app.
  • Private channels (private-), authorized by your backend with the signed-in user's token.
  • Client events on private and presence channels, for typing hints and other small signals.
  • Hibira Push for the moments the app is in the background or closed.

Step 1: install

flutter pub add dart_pusher_channels

Step 2: one client for the app

// lib/realtime.dart
import 'package:dart_pusher_channels/dart_pusher_channels.dart';

final realtime = PusherChannelsClient.websocket(
  options: PusherChannelsOptions.fromHost(
    scheme: 'wss',
    host: 'ws.hibira.io',
    port: 443,
    key: const String.fromEnvironment('HIBIRA_KEY'),
  ),
  connectionErrorHandler: (error, trace, refresh) => refresh(),
);

Pass the key when you build: flutter run --dart-define=HIBIRA_KEY=YOUR_APP_KEY. The app secret never goes into the app; private channels are signed by your backend.

Step 3: follow one order on its screen

// lib/order_screen.dart
import 'dart:async';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:flutter/material.dart';
import 'realtime.dart';

class OrderScreen extends StatefulWidget {
  const OrderScreen({super.key, required this.orderId, required this.token});
  final String orderId;
  final String token;

  @override
  State<OrderScreen> createState() => _OrderScreenState();
}

class _OrderScreenState extends State<OrderScreen> {
  late final PrivateChannel channel;
  final subs = <StreamSubscription>[];
  String status = '…';

  @override
  void initState() {
    super.initState();
    channel = realtime.privateChannel(
      'private-order-${widget.orderId}',
      authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
        authorizationEndpoint: Uri.parse('https://api.example.com/pusher/auth'),
        headers: {'Authorization': 'Bearer ${widget.token}'},
      ),
    );
    subs.add(channel.bind('status').listen((event) {
      setState(() => status = event.tryGetDataAsMap()?['status'] ?? status);
    }));
    subs.add(realtime.onConnectionEstablished.listen((_) => channel.subscribeIfNotUnsubscribed()));
    channel.subscribeIfNotUnsubscribed();
  }

  @override
  void dispose() {
    for (final s in subs) {
      s.cancel();
    }
    channel.unsubscribe();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Text(status);
}

Subscribing again on onConnectionEstablished puts the channel back after every reconnect. Your backend's auth endpoint receives socket_id and channel_name, checks the token, and returns the signature from any Pusher-compatible server library.

Step 4: connect while the app is open

class _AppState extends State<App> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    realtime.connect();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) realtime.disconnect();
    if (state == AppLifecycleState.resumed) realtime.connect();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => const MaterialApp(home: HomeScreen());
}

iOS and Android suspend a background app's network anyway, so disconnect on paused and connect again on resumed. Anything the user must hear about while the app is closed should go out as a push notification: see push notifications that bring users back and the push guide, which covers the Android and iOS SDKs your Flutter host app can call.

Tips

  • Load what the screen needs from your API first, then let events update it; for state that must be there on open, publish to a cache- channel.
  • Client events (channel.trigger(eventName: 'client-typing', data: {...})) work on private and presence channels once Allow client events is on in the app settings.
  • The Flutter guide has the client settings filled in with your app's values once you sign in.