system_live_updates 0.1.0 copy "system_live_updates: ^0.1.0" to clipboard
system_live_updates: ^0.1.0 copied to clipboard

A Flutter plugin for iOS Live Activities and Android live progress notifications with one Dart lifecycle API.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:system_live_updates/system_live_updates.dart';

void main() {
  runApp(const ExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1E7A57)),
        useMaterial3: true,
      ),
      home: const LiveUpdateDemo(),
    );
  }
}

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

  @override
  State<LiveUpdateDemo> createState() => _LiveUpdateDemoState();
}

class _LiveUpdateDemoState extends State<LiveUpdateDemo> {
  final _liveUpdates = const SystemLiveUpdates();

  LiveUpdateCapabilities? _capabilities;
  LiveUpdatePermissionStatus? _permissions;
  IosLiveActivityTemplate _template = IosLiveActivityTemplate.journey;
  double _progress = 0.18;
  String _message = 'Ready';

  String get _activityId => 'demo_${_template.name}';

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

  Future<void> _loadCapabilities() async {
    try {
      final capabilities = await _liveUpdates.capabilities();
      if (!mounted) {
        return;
      }
      setState(() {
        _capabilities = capabilities;
      });
    } on PlatformException catch (error) {
      _showError(error);
    }
  }

  Future<void> _start() async {
    await _run(() async {
      final permissions = await _liveUpdates.requestPermissions();
      if (!mounted) {
        return;
      }
      setState(() {
        _permissions = permissions;
      });
      if (!permissions.notificationsEnabled ||
          permissions.liveActivitiesEnabled == false) {
        setState(() {
          _message = 'Notifications or Live Activities are disabled';
        });
        return;
      }

      await _liveUpdates.start(_content(_presetCopy.startSubtitle));
      setState(() {
        _message = 'Started ${_template.name} demo';
      });
    });
  }

  Future<void> _openSettings() async {
    await _run(() async {
      final opened = await _liveUpdates.openSettings();
      setState(() {
        _message = opened ? 'Opened app settings' : 'Could not open settings';
      });
    });
  }

  Future<void> _advance() async {
    await _run(() async {
      setState(() {
        _progress = (_progress + 0.22).clamp(0, 1);
      });
      await _liveUpdates.update(_content(_presetCopy.updateSubtitle));
      setState(() {
        _message = 'Updated ${_template.name} demo';
      });
    });
  }

  Future<void> _end() async {
    await _run(() async {
      await _liveUpdates.end(
        _activityId,
        title: _presetCopy.doneTitle,
        subtitle: _presetCopy.doneSubtitle,
      );
      setState(() {
        _progress = 1;
        _message = 'Ended ${_template.name} demo';
      });
    });
  }

  Future<void> _run(Future<void> Function() action) async {
    try {
      await action();
      await _loadCapabilities();
    } on PlatformException catch (error) {
      _showError(error);
    }
  }

  LiveUpdateContent _content(String subtitle) {
    final preset = _presetCopy;
    return LiveUpdateContent(
      id: _activityId,
      title: preset.title,
      subtitle: subtitle,
      progress: _progress,
      progressOptions: LiveUpdateProgressOptions(
        shortStatus: '${(_progress * 100).round()}%',
        segments: const [
          LiveUpdateSegment(length: 30, color: 0xFF4F8DFF),
          LiveUpdateSegment(length: 70, color: 0xFF1E7A57),
        ],
        points: const [LiveUpdatePoint(position: 30, color: 0xFFFFB000)],
      ),
      eta: DateTime.now().add(preset.eta),
      deepLink: Uri.parse('system-live-updates-example://${_template.name}'),
      android: const AndroidLiveUpdateOptions(),
      ios: IosLiveActivityOptions(
        template: _template,
        layout: preset.layout,
        symbolName: preset.symbolName,
        accentColor: preset.accentColor,
        leadingLabel: preset.leadingLabel,
        trailingLabel: preset.trailingLabel,
        bottomLabel: preset.bottomLabel,
        primaryMetric: preset.primaryMetric,
        secondaryMetric: preset.secondaryMetric,
        showsProgressRing: preset.showsProgressRing,
        showsEta: preset.showsEta,
        relevanceScore: 90,
        staleAfter: const Duration(minutes: 30),
      ),
    );
  }

  _PresetCopy get _presetCopy {
    switch (_template) {
      case IosLiveActivityTemplate.progress:
        return const _PresetCopy(
          title: 'Video Upload',
          startSubtitle: 'Uploading file',
          updateSubtitle: 'Processing video',
          doneTitle: 'Upload complete',
          doneSubtitle: 'Ready to share',
          symbolName: 'arrow.up.circle.fill',
          accentColor: 0xFF4F8DFF,
          leadingLabel: 'Upload',
          trailingLabel: '2 min',
          bottomLabel: 'Processing in the background',
          primaryMetric: '43%',
          secondaryMetric: '2 min left',
        );
      case IosLiveActivityTemplate.journey:
        return const _PresetCopy(
          title: 'Tungworks Delivery',
          startSubtitle: 'Driver is picking up your food',
          updateSubtitle: 'Driver is on the way',
          doneTitle: 'Delivered',
          doneSubtitle: 'Enjoy your order',
          layout: IosLiveActivityLayout.prominent,
          symbolName: 'takeoutbag.and.cup.and.straw',
          accentColor: 0xFF1E7A57,
          leadingLabel: 'Pickup',
          trailingLabel: '18 min',
          bottomLabel: 'Driver is heading to your address',
          primaryMetric: '18 min',
          secondaryMetric: '2.4 km',
        );
      case IosLiveActivityTemplate.timer:
        return const _PresetCopy(
          title: 'Focus Session',
          startSubtitle: 'Deep work in progress',
          updateSubtitle: 'Keep going',
          doneTitle: 'Session complete',
          doneSubtitle: 'Nice work',
          layout: IosLiveActivityLayout.compact,
          symbolName: 'timer',
          accentColor: 0xFFFF9500,
          leadingLabel: 'Focus',
          trailingLabel: '12:30',
          bottomLabel: 'Break starts soon',
          primaryMetric: '12:30',
          secondaryMetric: 'remaining',
          eta: Duration(minutes: 13),
        );
      case IosLiveActivityTemplate.status:
        return const _PresetCopy(
          title: 'Table Booking',
          startSubtitle: 'Checking availability',
          updateSubtitle: 'You are next in queue',
          doneTitle: 'Table ready',
          doneSubtitle: 'Please head to the host stand',
          symbolName: 'person.2.fill',
          accentColor: 0xFF5856D6,
          leadingLabel: 'Queue',
          trailingLabel: '#2',
          bottomLabel: 'We will hold your table for 10 minutes',
          primaryMetric: '#2',
          secondaryMetric: 'in queue',
        );
      case IosLiveActivityTemplate.minimal:
        return const _PresetCopy(
          title: 'Task Running',
          startSubtitle: 'Working in background',
          updateSubtitle: 'Almost done',
          doneTitle: 'Task complete',
          doneSubtitle: 'Finished successfully',
          layout: IosLiveActivityLayout.compact,
          symbolName: 'checkmark.circle.fill',
          accentColor: 0xFF34C759,
          leadingLabel: 'Task',
          trailingLabel: 'Live',
          bottomLabel: 'Finishing up',
          showsProgressRing: false,
          showsEta: false,
        );
    }
  }

  void _showError(PlatformException error) {
    if (!mounted) {
      return;
    }
    setState(() {
      _message = '${error.code}: ${error.message ?? 'Unknown error'}';
    });
  }

  @override
  Widget build(BuildContext context) {
    final capabilities = _capabilities;
    final permissions = _permissions;

    return Scaffold(
      appBar: AppBar(title: const Text('System Live Updates')),
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            capabilities == null
                ? 'Checking support...'
                : capabilities.surface.name,
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 8),
          Text(_message),
          const SizedBox(height: 24),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: SegmentedButton<IosLiveActivityTemplate>(
              showSelectedIcon: false,
              style: const ButtonStyle(
                padding: WidgetStatePropertyAll(
                  EdgeInsets.symmetric(horizontal: 12),
                ),
                visualDensity: VisualDensity.compact,
              ),
              segments: IosLiveActivityTemplate.values
                  .map(
                    (template) => ButtonSegment<IosLiveActivityTemplate>(
                      value: template,
                      label: Text(template.name),
                    ),
                  )
                  .toList(),
              selected: {_template},
              onSelectionChanged: (selection) {
                setState(() {
                  _template = selection.single;
                  _progress = 0.18;
                  _message = 'Ready';
                });
              },
            ),
          ),
          const SizedBox(height: 24),
          LinearProgressIndicator(value: _progress),
          const SizedBox(height: 24),
          Wrap(
            spacing: 12,
            runSpacing: 12,
            children: [
              FilledButton.icon(
                onPressed: _start,
                icon: const Icon(Icons.play_arrow),
                label: const Text('Start'),
              ),
              FilledButton.tonalIcon(
                onPressed: _advance,
                icon: const Icon(Icons.trending_flat),
                label: const Text('Update'),
              ),
              OutlinedButton.icon(
                onPressed: _end,
                icon: const Icon(Icons.stop),
                label: const Text('End'),
              ),
              OutlinedButton.icon(
                onPressed: _openSettings,
                icon: const Icon(Icons.settings),
                label: const Text('Settings'),
              ),
            ],
          ),
          const SizedBox(height: 24),
          if (capabilities != null)
            Table(
              columnWidths: const {
                0: IntrinsicColumnWidth(),
                1: FlexColumnWidth(),
              },
              children: [
                _row('Platform', capabilities.platform),
                _row('OS', capabilities.osVersion ?? '-'),
                _row('SDK', capabilities.sdkInt?.toString() ?? '-'),
                _row(
                  'Notifications',
                  capabilities.notificationsEnabled.toString(),
                ),
                if (permissions != null)
                  _row('Permission', permissions.authorizationStatus ?? '-'),
                if (permissions?.liveActivitiesEnabled != null)
                  _row(
                    'Live Activities',
                    permissions!.liveActivitiesEnabled.toString(),
                  ),
              ],
            ),
        ],
      ),
    );
  }

  TableRow _row(String label, String value) {
    return TableRow(
      children: [
        Padding(
          padding: const EdgeInsets.only(right: 16, bottom: 8),
          child: Text(
            label,
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
        Padding(padding: const EdgeInsets.only(bottom: 8), child: Text(value)),
      ],
    );
  }
}

class _PresetCopy {
  const _PresetCopy({
    required this.title,
    required this.startSubtitle,
    required this.updateSubtitle,
    required this.doneTitle,
    required this.doneSubtitle,
    required this.symbolName,
    required this.accentColor,
    required this.leadingLabel,
    required this.trailingLabel,
    required this.bottomLabel,
    this.primaryMetric,
    this.secondaryMetric,
    this.layout = IosLiveActivityLayout.standard,
    this.showsProgressRing = true,
    this.showsEta = true,
    this.eta = const Duration(minutes: 18),
  });

  final String title;
  final String startSubtitle;
  final String updateSubtitle;
  final String doneTitle;
  final String doneSubtitle;
  final String symbolName;
  final int accentColor;
  final String leadingLabel;
  final String trailingLabel;
  final String bottomLabel;
  final String? primaryMetric;
  final String? secondaryMetric;
  final IosLiveActivityLayout layout;
  final bool showsProgressRing;
  final bool showsEta;
  final Duration eta;
}
0
likes
160
points
47
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for iOS Live Activities and Android live progress notifications with one Dart lifecycle API.

Homepage
Repository (GitHub)
View/report issues

Topics

#live-activities #live-updates #notifications #activitykit #android16

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on system_live_updates

Packages that implement system_live_updates