foreground_background_manager 1.0.0
foreground_background_manager: ^1.0.0 copied to clipboard
A production-ready Flutter plugin for foreground/background detection, Android foreground services, background task scheduling, and lifecycle management.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:foreground_background_manager/foreground_background_manager.dart';
// ─── Top-level callback dispatcher (MUST be top-level) ────────────────────────
@pragma('vm:entry-point')
void callbackDispatcher() {
ForegroundBackgroundManager.backgroundCallbackDispatcher();
}
// ─── Top-level background task (MUST be top-level) ────────────────────────────
@pragma('vm:entry-point')
Future<void> _syncCallback() async {
debugPrint('[FBM Example] Background sync running...');
await Future.delayed(const Duration(seconds: 1));
debugPrint('[FBM Example] Background sync done.');
}
// ─── Main ──────────────────────────────────────────────────────────────────────
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ForegroundBackgroundManager.initialize(
callbackDispatcher: callbackDispatcher,
);
runApp(const FBMExampleApp());
}
// ─── App ───────────────────────────────────────────────────────────────────────
class FBMExampleApp extends StatelessWidget {
const FBMExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FBM Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF07071A),
colorScheme: ColorScheme.dark(
primary: const Color(0xFF7C3AED),
secondary: const Color(0xFF4F46E5),
surface: const Color(0xFF111132),
),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
// ─── Home Screen ───────────────────────────────────────────────────────────────
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
// State
FBMLifecycleState _lifecycleState = FBMLifecycleState.resumed;
bool _bgTaskRunning = false;
bool _fgSvcRunning = false;
bool _batteryIgnored = false;
final List<_Log> _logs = [];
// Subscriptions
final _subs = <StreamSubscription>[];
// ─── Lifecycle ─────────────────────────────────────────────────────────
@override
void initState() {
super.initState();
_setupStreams();
_checkBatteryStatus();
_addLog('Plugin initialized ✅', LogLevel.success);
}
void _setupStreams() {
_subs.add(ForegroundBackgroundManager.lifecycleStream.listen((s) {
setState(() => _lifecycleState = s);
_addLog('Lifecycle → ${s.label}', LogLevel.lifecycle);
}));
_subs.add(ForegroundBackgroundManager.onForeground.listen((_) {
_addLog('App entered FOREGROUND 🌅', LogLevel.success);
}));
_subs.add(ForegroundBackgroundManager.onBackground.listen((_) {
_addLog('App entered BACKGROUND 🌙', LogLevel.warning);
}));
_subs.add(ForegroundBackgroundManager.onAppKilled.listen((_) {
_addLog('App KILLED — service will restart 🔄', LogLevel.warning);
}));
_subs.add(ForegroundBackgroundManager.onNotificationClick.listen((_) {
_addLog('Notification tapped 🔔', LogLevel.info);
}));
}
@override
void dispose() {
for (final s in _subs) { s.cancel(); }
ForegroundBackgroundManager.dispose();
super.dispose();
}
// ─── Actions ───────────────────────────────────────────────────────────
Future<void> _startBgTask() async {
try {
await ForegroundBackgroundManager.registerBackgroundTask(
taskId: 'sync_task',
interval: const Duration(minutes: 15),
callback: _syncCallback,
);
setState(() => _bgTaskRunning = true);
_addLog('Background task registered 🔄', LogLevel.success);
} catch (e) {
_addLog('Register task failed: $e', LogLevel.error);
}
}
Future<void> _stopBgTask() async {
try {
await ForegroundBackgroundManager.cancelBackgroundTask('sync_task');
setState(() => _bgTaskRunning = false);
_addLog('Background task cancelled ⛔', LogLevel.warning);
} catch (e) {
_addLog('Cancel task failed: $e', LogLevel.error);
}
}
Future<void> _startFgService() async {
try {
// Request notification permission first (Android 13+)
final status = await ForegroundBackgroundManager.permissions
.requestNotificationPermission();
_addLog('Notification permission: $status', LogLevel.info);
if (status == PermissionStatus.denied ||
status == PermissionStatus.permanentlyDenied) {
_addLog('Permission denied — cannot start service', LogLevel.error);
return;
}
await ForegroundBackgroundManager.startForegroundService(
title: 'FBM Running',
content: 'Service is active — even if app is killed',
priority: NotificationPriority.low,
autoRestart: true,
);
setState(() => _fgSvcRunning = true);
_addLog('Foreground service started 🚀', LogLevel.success);
_addLog('Kill the app — service will restart automatically!', LogLevel.info);
} catch (e) {
_addLog('Start service failed: $e', LogLevel.error);
}
}
Future<void> _stopFgService() async {
try {
await ForegroundBackgroundManager.stopForegroundService();
setState(() => _fgSvcRunning = false);
_addLog('Foreground service stopped 🛑', LogLevel.warning);
} catch (e) {
_addLog('Stop service failed: $e', LogLevel.error);
}
}
Future<void> _checkBatteryStatus() async {
final ignored = await ForegroundBackgroundManager.permissions
.isBatteryOptimizationIgnored();
setState(() => _batteryIgnored = ignored);
}
Future<void> _requestBatteryIgnore() async {
_addLog('Opening battery optimization settings...', LogLevel.info);
await ForegroundBackgroundManager.permissions.requestIgnoreBatteryOptimization();
await Future.delayed(const Duration(seconds: 1));
await _checkBatteryStatus();
if (_batteryIgnored) {
_addLog('Battery optimization ignored ✅ — service more reliable!', LogLevel.success);
} else {
_addLog('Battery optimization still active ⚠️', LogLevel.warning);
}
}
Future<void> _updateNotification() async {
if (!_fgSvcRunning) {
_addLog('Service not running', LogLevel.error);
return;
}
await ForegroundBackgroundManager.updateForegroundNotification(
title: 'FBM Updated',
content: 'Notification updated at ${TimeOfDay.now().format(context)}',
);
_addLog('Notification updated ✏️', LogLevel.info);
}
void _clearLogs() => setState(() => _logs.clear());
void _addLog(String msg, LogLevel lvl) {
setState(() {
_logs.insert(0, _Log(msg, lvl, DateTime.now()));
if (_logs.length > 150) _logs.removeLast();
});
}
// ─── Build ─────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF07071A),
body: SafeArea(
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverToBoxAdapter(child: _header()),
SliverToBoxAdapter(child: _statusCard()),
SliverToBoxAdapter(child: _batteryBanner()),
SliverToBoxAdapter(child: _sectionLabel('Foreground Service')),
SliverToBoxAdapter(child: _serviceControls()),
SliverToBoxAdapter(child: _sectionLabel('Background Task')),
SliverToBoxAdapter(child: _bgTaskControls()),
SliverToBoxAdapter(child: _sectionLabel('Live Logs', extra: _logs.length)),
SliverToBoxAdapter(child: _logConsole()),
const SliverToBoxAdapter(child: SizedBox(height: 40)),
],
),
),
);
}
// ─── Widget helpers ────────────────────────────────────────────────────
Widget _header() => Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 6),
child: Row(
children: [
_gradientIcon(Icons.layers_rounded),
const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('ForegroundBackground', style: TextStyle(
color: Colors.white, fontSize: 17, fontWeight: FontWeight.w700)),
Text('Manager Demo', style: TextStyle(
color: Colors.white.withOpacity(0.45), fontSize: 12)),
]),
const Spacer(),
_PulsingDot(active: _lifecycleState.isForegrounded),
],
),
);
Widget _statusCard() => Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: _GlassCard(
child: Row(children: [
AnimatedContainer(
duration: const Duration(milliseconds: 350),
width: 50, height: 50,
decoration: BoxDecoration(
color: _lifecycleState.isForegrounded
? const Color(0xFF10B981).withOpacity(0.15)
: const Color(0xFFF59E0B).withOpacity(0.15),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
_lifecycleState.isForegrounded ? Icons.visibility_rounded : Icons.bedtime_rounded,
color: _lifecycleState.isForegrounded
? const Color(0xFF10B981) : const Color(0xFFF59E0B),
size: 24,
),
),
const SizedBox(width: 14),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('App State', style: TextStyle(
color: Colors.white.withOpacity(0.45), fontSize: 11, letterSpacing: 0.6)),
const SizedBox(height: 3),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: Text(_lifecycleState.label, key: ValueKey(_lifecycleState),
style: TextStyle(
color: _lifecycleState.isForegrounded
? const Color(0xFF10B981) : const Color(0xFFF59E0B),
fontSize: 16, fontWeight: FontWeight.w700,
)),
),
]),
const Spacer(),
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
_Chip(label: 'BG Task', active: _bgTaskRunning, color: const Color(0xFF7C3AED)),
const SizedBox(height: 6),
_Chip(label: 'FG Service', active: _fgSvcRunning, color: const Color(0xFF4F46E5)),
]),
]),
),
);
Widget _batteryBanner() {
if (_batteryIgnored) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 4),
child: GestureDetector(
onTap: _requestBatteryIgnore,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withOpacity(0.12),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFF59E0B).withOpacity(0.4)),
),
child: Row(children: [
const Icon(Icons.battery_alert_rounded, color: Color(0xFFF59E0B), size: 20),
const SizedBox(width: 10),
Expanded(child: Text(
'Tap to disable battery optimization — required for kill-proof service',
style: const TextStyle(color: Color(0xFFF59E0B), fontSize: 12),
)),
const Icon(Icons.chevron_right_rounded, color: Color(0xFFF59E0B), size: 18),
]),
),
),
);
}
Widget _sectionLabel(String label, {int? extra}) => Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 6),
child: Row(children: [
Text(label.toUpperCase(), style: TextStyle(
color: Colors.white.withOpacity(0.4), fontSize: 11,
fontWeight: FontWeight.w700, letterSpacing: 1.3)),
if (extra != null) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFF7C3AED).withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text('$extra', style: const TextStyle(
color: Color(0xFF7C3AED), fontSize: 10, fontWeight: FontWeight.w700)),
),
],
]),
);
Widget _serviceControls() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _GlassCard(
padding: const EdgeInsets.all(14),
child: Column(children: [
Row(children: [
Expanded(child: _ActionBtn(
label: _fgSvcRunning ? 'Stop Service' : 'Start Service',
icon: _fgSvcRunning ? Icons.stop_circle_outlined : Icons.rocket_launch_rounded,
color: const Color(0xFF4F46E5),
active: _fgSvcRunning,
onTap: _fgSvcRunning ? _stopFgService : _startFgService,
)),
const SizedBox(width: 10),
Expanded(child: _ActionBtn(
label: 'Update Text',
icon: Icons.edit_notifications_rounded,
color: const Color(0xFF0891B2),
active: false,
onTap: _updateNotification,
)),
]),
const SizedBox(height: 10),
_InfoBox(
icon: Icons.info_outline_rounded,
text: _fgSvcRunning
? 'Service running — try minimizing or killing the app. It will restart!'
: 'Start the service. It persists through minimize, background, and kill.',
color: _fgSvcRunning ? const Color(0xFF10B981) : Colors.white38,
),
]),
),
);
Widget _bgTaskControls() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _GlassCard(
padding: const EdgeInsets.all(14),
child: Column(children: [
Row(children: [
Expanded(child: _ActionBtn(
label: _bgTaskRunning ? 'Stop Task' : 'Start Task',
icon: _bgTaskRunning ? Icons.stop_circle_outlined : Icons.sync_rounded,
color: const Color(0xFF7C3AED),
active: _bgTaskRunning,
onTap: _bgTaskRunning ? _stopBgTask : _startBgTask,
)),
const SizedBox(width: 10),
Expanded(child: _ActionBtn(
label: 'Clear Logs',
icon: Icons.delete_sweep_rounded,
color: const Color(0xFFDC2626),
active: false,
onTap: _clearLogs,
)),
]),
const SizedBox(height: 10),
_InfoBox(
icon: Icons.schedule_rounded,
text: 'WorkManager (Android) / BGTaskScheduler (iOS). Minimum: 15 min.',
color: Colors.white38,
),
]),
),
);
Widget _logConsole() => Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: _GlassCard(
padding: EdgeInsets.zero,
child: _logs.isEmpty
? const Padding(
padding: EdgeInsets.all(28),
child: Center(child: Text('No logs yet...',
style: TextStyle(color: Colors.white24, fontSize: 13,
fontStyle: FontStyle.italic))))
: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 340),
child: ListView.separated(
padding: const EdgeInsets.all(12),
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: _logs.length,
separatorBuilder: (_, __) =>
Divider(color: Colors.white.withOpacity(0.05), height: 1),
itemBuilder: (_, i) => _LogTile(entry: _logs[i]),
),
),
),
);
Widget _gradientIcon(IconData icon) => Container(
width: 40, height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF7C3AED), Color(0xFF4F46E5)]),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: Colors.white, size: 20),
);
}
// ─── Reusable Widgets ──────────────────────────────────────────────────────────
class _GlassCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
const _GlassCard({required this.child, this.padding});
@override
Widget build(BuildContext context) => Container(
padding: padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.04),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withOpacity(0.08)),
boxShadow: [BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 16, offset: const Offset(0, 4))],
),
child: child,
);
}
class _ActionBtn extends StatelessWidget {
final String label;
final IconData icon;
final Color color;
final bool active;
final VoidCallback onTap;
const _ActionBtn({required this.label, required this.icon,
required this.color, required this.active, required this.onTap});
@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: active ? color.withOpacity(0.22) : color.withOpacity(0.09),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: active
? color.withOpacity(0.55) : color.withOpacity(0.2)),
),
child: Column(children: [
Icon(icon, color: color, size: 22),
const SizedBox(height: 6),
Text(label, textAlign: TextAlign.center,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w600)),
]),
),
);
}
class _Chip extends StatelessWidget {
final String label;
final bool active;
final Color color;
const _Chip({required this.label, required this.active, required this.color});
@override
Widget build(BuildContext context) => AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
decoration: BoxDecoration(
color: active ? color.withOpacity(0.18) : Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: active ? color.withOpacity(0.45) : Colors.white.withOpacity(0.1)),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Container(width: 6, height: 6,
decoration: BoxDecoration(
color: active ? color : Colors.white24, shape: BoxShape.circle)),
const SizedBox(width: 5),
Text(label, style: TextStyle(
color: active ? color : Colors.white38, fontSize: 10, fontWeight: FontWeight.w600)),
]),
);
}
class _InfoBox extends StatelessWidget {
final IconData icon;
final String text;
final Color color;
const _InfoBox({required this.icon, required this.text, required this.color});
@override
Widget build(BuildContext context) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 6),
Expanded(child: Text(text, style: TextStyle(color: color, fontSize: 11))),
],
);
}
class _PulsingDot extends StatefulWidget {
final bool active;
const _PulsingDot({required this.active});
@override
State<_PulsingDot> createState() => _PulsingDotState();
}
class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin {
late final AnimationController _ctrl = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1100))..repeat(reverse: true);
late final Animation<double> _anim =
Tween(begin: 0.35, end: 1.0).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
@override
void dispose() { _ctrl.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
final c = widget.active ? const Color(0xFF10B981) : const Color(0xFFF59E0B);
return FadeTransition(
opacity: _anim,
child: Container(width: 9, height: 9,
decoration: BoxDecoration(color: c, shape: BoxShape.circle)),
);
}
}
// ─── Log models ────────────────────────────────────────────────────────────────
enum LogLevel { info, success, warning, error, lifecycle }
class _Log {
final String msg;
final LogLevel level;
final DateTime time;
_Log(this.msg, this.level, this.time);
}
class _LogTile extends StatelessWidget {
final _Log entry;
const _LogTile({required this.entry});
@override
Widget build(BuildContext context) {
final color = _color(entry.level);
final t = entry.time;
final ts = '${t.hour.toString().padLeft(2,'0')}:'
'${t.minute.toString().padLeft(2,'0')}:'
'${t.second.toString().padLeft(2,'0')}';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(_icon(entry.level), style: const TextStyle(fontSize: 13)),
const SizedBox(width: 7),
Expanded(child: Text(entry.msg, style: TextStyle(
color: color, fontSize: 11.5, fontWeight: FontWeight.w500))),
Text(ts, style: const TextStyle(
color: Colors.white24, fontSize: 10, fontFamily: 'monospace')),
]),
);
}
Color _color(LogLevel l) {
switch (l) {
case LogLevel.info: return Colors.white60;
case LogLevel.success: return const Color(0xFF10B981);
case LogLevel.warning: return const Color(0xFFF59E0B);
case LogLevel.error: return const Color(0xFFEF4444);
case LogLevel.lifecycle: return const Color(0xFF818CF8);
}
}
String _icon(LogLevel l) {
switch (l) {
case LogLevel.info: return 'ℹ️';
case LogLevel.success: return '✅';
case LogLevel.warning: return '⚠️';
case LogLevel.error: return '❌';
case LogLevel.lifecycle: return '🔄';
}
}
}