system_live_updates 0.1.0
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.
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;
}