live_activity_kit 1.1.0 copy "live_activity_kit: ^1.1.0" to clipboard
live_activity_kit: ^1.1.0 copied to clipboard

Build iOS Live Activities and Dynamic Island widgets, plus Android Live Updates / ongoing notifications, from Dart. Component DSL, SwiftUI renderer, ActivityKit, App Groups and APNs push updates — no [...]

example/lib/main.dart

import 'dart:async';

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

import 'demos/delivery_tracking.dart';
import 'demos/demo.dart';
import 'demos/meal_reminder.dart';
import 'demos/meeting_countdown.dart';
import 'demos/running_tracker.dart';

void main() => runApp(const ExampleApp());

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

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'live_activity_kit',
        debugShowCheckedModeBanner: false,
        theme: ThemeData.dark(useMaterial3: true).copyWith(
          scaffoldBackgroundColor: const Color(0xFF0B0B0F),
        ),
        home: const HomePage(),
      );
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late final List<LiveActivityDemo> _demos = [
    MealReminderDemo(),
    RunningTrackerDemo(),
    DeliveryTrackingDemo(
        onPushToken: (token) => _log('push token ${_short(token)}')),
    MeetingCountdownDemo(),
  ];

  final Set<String> _running = {};
  final List<String> _events = [];
  final List<StreamSubscription<void>> _subscriptions = [];

  LiveActivitySupport? _support;

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

    // Lifecycle and deep links are streams, so the app stays in sync even when
    // the user dismisses an activity from the Lock Screen.
    _subscriptions.addAll([
      LiveActivity.states.listen((change) {
        _log('${change.id} → ${change.state.name}');
        if (change.state != LiveActivityState.active) {
          setState(() => _running.remove(change.id));
        }
      }),
      LiveActivity.deepLinks.listen((url) => _log('tapped $url')),
    ]);
  }

  @override
  void dispose() {
    for (final subscription in _subscriptions) {
      subscription.cancel();
    }
    super.dispose();
  }

  Future<void> _checkSupport() async {
    final support = await LiveActivity.support(refresh: true);
    if (mounted) setState(() => _support = support);
  }

  void _log(String message) {
    if (!mounted) return;
    setState(() {
      _events.insert(0, message);
      if (_events.length > 12) _events.removeLast();
    });
  }

  static String _short(String token) =>
      token.length <= 12 ? token : '${token.substring(0, 12)}…';

  Future<void> _run(
    LiveActivityDemo demo,
    Future<void> Function() action,
    String label,
  ) async {
    try {
      await action();
      _log('$label ${demo.id}');
    } on LiveActivityException catch (e) {
      _log('${e.code}: ${e.message}');
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
            content: Text(e.message), backgroundColor: Colors.red.shade900),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('live_activity_kit'),
        backgroundColor: Colors.transparent,
      ),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
        children: [
          _SupportBanner(support: _support, onRefresh: _checkSupport),
          const SizedBox(height: 16),
          for (final demo in _demos) ...[
            _DemoCard(
              demo: demo,
              isRunning: _running.contains(demo.id),
              onStart: () async {
                await _run(demo, demo.start, 'started');
                setState(() => _running.add(demo.id));
              },
              onUpdate: () => _run(demo, demo.update, 'updated'),
              onEnd: () async {
                await _run(demo, demo.end, 'ended');
                demo.reset();
                setState(() => _running.remove(demo.id));
              },
            ),
            const SizedBox(height: 12),
          ],
          const SizedBox(height: 8),
          _EventLog(events: _events),
          const SizedBox(height: 16),
          OutlinedButton.icon(
            onPressed: () async {
              await LiveActivity.endAll(immediate: true);
              for (final demo in _demos) {
                demo.reset();
              }
              setState(_running.clear);
              _log('ended everything');
            },
            icon: const Icon(Icons.stop_circle_outlined),
            label: const Text('End all activities'),
          ),
        ],
      ),
    );
  }
}

class _SupportBanner extends StatelessWidget {
  const _SupportBanner({required this.support, required this.onRefresh});

  final LiveActivitySupport? support;
  final VoidCallback onRefresh;

  @override
  Widget build(BuildContext context) {
    final support = this.support;
    if (support == null) {
      return const LinearProgressIndicator(minHeight: 2);
    }

    final (color, icon, message) = switch (support) {
      _ when !support.isSupported => (
          Colors.red,
          Icons.error_outline,
          'Live Activities need iOS 16.1+ or Android API 26+. This device reports ${support.systemVersion}.',
        ),
      _ when !support.areActivitiesEnabled => (
          Colors.orange,
          Icons.warning_amber_outlined,
          support.systemVersion.startsWith('Android')
              ? 'Notifications are off for this app. Enable them in Settings, then refresh.'
              : 'Live Activities are switched off for this app in Settings → live_activity_kit.',
        ),
      _ => (
          Colors.green,
          Icons.check_circle_outline,
          support.supportsDynamicIsland
              ? 'Ready — this device has a Dynamic Island.'
              : support.supportsLiveUpdates
                  ? 'Ready — Android Live Updates (status bar / lock screen).'
                  : support.systemVersion.startsWith('Android')
                      ? 'Ready — Android ongoing notification (Live Updates need Android 16+).'
                      : 'Ready — Lock Screen only (no Dynamic Island on this device).',
        ),
    };

    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: color.withValues(alpha: 0.35)),
      ),
      child: Row(
        children: [
          Icon(icon, color: color, size: 20),
          const SizedBox(width: 10),
          Expanded(child: Text(message, style: const TextStyle(fontSize: 13))),
          IconButton(
            onPressed: onRefresh,
            icon: const Icon(Icons.refresh, size: 18),
            tooltip: 'Re-check',
          ),
        ],
      ),
    );
  }
}

class _DemoCard extends StatelessWidget {
  const _DemoCard({
    required this.demo,
    required this.isRunning,
    required this.onStart,
    required this.onUpdate,
    required this.onEnd,
  });

  final LiveActivityDemo demo;
  final bool isRunning;
  final VoidCallback onStart;
  final VoidCallback onUpdate;
  final VoidCallback onEnd;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: const Color(0xFF15151C),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: isRunning ? demo.accent : Colors.white10,
          width: isRunning ? 1.5 : 1,
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(demo.icon, color: demo.accent),
              const SizedBox(width: 10),
              Expanded(
                child: Text(
                  demo.title,
                  style: const TextStyle(
                      fontSize: 16, fontWeight: FontWeight.w600),
                ),
              ),
              if (isRunning)
                Container(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
                  decoration: BoxDecoration(
                    color: demo.accent.withValues(alpha: 0.18),
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: Text('LIVE',
                      style: TextStyle(
                          fontSize: 10,
                          fontWeight: FontWeight.w700,
                          color: demo.accent)),
                ),
            ],
          ),
          const SizedBox(height: 6),
          Text(demo.description,
              style: const TextStyle(fontSize: 12, color: Colors.white54)),
          const SizedBox(height: 14),
          Row(
            children: [
              Expanded(
                child: FilledButton(
                  onPressed: isRunning ? null : onStart,
                  style: FilledButton.styleFrom(backgroundColor: demo.accent),
                  child: const Text('Start'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton(
                  onPressed: isRunning ? onUpdate : null,
                  child: const Text('Update'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton(
                  onPressed: isRunning ? onEnd : null,
                  child: const Text('End'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _EventLog extends StatelessWidget {
  const _EventLog({required this.events});

  final List<String> events;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: Colors.black26,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('Events',
              style: TextStyle(fontSize: 12, color: Colors.white38)),
          const SizedBox(height: 8),
          if (events.isEmpty)
            const Text('—', style: TextStyle(color: Colors.white24))
          else
            for (final event in events)
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 1),
                child: Text(
                  event,
                  style: const TextStyle(
                      fontSize: 12, fontFamily: 'Menlo', color: Colors.white70),
                ),
              ),
        ],
      ),
    );
  }
}
5
likes
140
points
200
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Build iOS Live Activities and Dynamic Island widgets, plus Android Live Updates / ongoing notifications, from Dart. Component DSL, SwiftUI renderer, ActivityKit, App Groups and APNs push updates — no Swift code.

Repository (GitHub)
View/report issues

Topics

#live-activities #dynamic-island #activitykit #ios #android

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on live_activity_kit

Packages that implement live_activity_kit