juice_lifecycle

App lifecycle — foreground / background / resume — as a Juice bloc, behind a swappable provider seam.

pub package License: MIT

What it owns

The app's AppLifecycle phase (resumed / inactive / paused / detached / hidden) plus the previous phase. It does not decide what to do on a transition — consumers react (e.g. re-check permissions on resume, reconnect a socket).

Install

dependencies:
  juice_lifecycle: ^0.1.0

Use

import 'package:juice/juice.dart';
import 'package:juice_lifecycle/juice_lifecycle.dart';

final lifecycle = LifecycleBloc.withConfig(LifecycleConfig());

class PrivacyShield extends StatelessJuiceWidget<LifecycleBloc> {
  PrivacyShield({super.key}) : super(groups: {LifecycleGroups.state});

  @override
  Widget onBuild(BuildContext context, StreamStatus status) {
    // Blur the app in the task switcher when not foreground.
    return bloc.state.isForeground ? const AppBody() : const BlurredScreen();
  }
}

state.resumedFromBackground is handy for "do X when the user comes back" — re-reading permissions, refreshing data, reconnecting.

State

Field / getter Meaning
lifecycle current AppLifecycle phase
previous phase before the current one
isForeground resumed
isBackground paused or hidden
resumedFromBackground just returned to foreground

The provider seam (and why it's testable)

LifecycleBloc depends on the LifecycleProvider interface, not on WidgetsBinding. The default WidgetsLifecycleProvider wraps Flutter's AppLifecycleListener; inject a fake in tests:

class FakeLifecycleProvider implements LifecycleProvider {
  final _ctrl = StreamController<AppLifecycle>.broadcast();
  AppLifecycle _current = AppLifecycle.resumed;

  @override Stream<AppLifecycle> get changes => _ctrl.stream;
  @override AppLifecycle get current => _current;
  @override Future<void> dispose() async => _ctrl.close();

  void emit(AppLifecycle p) { _current = p; _ctrl.add(p); }
}

License

MIT License — see LICENSE.

Libraries

juice_lifecycle
App lifecycle (foreground/background/resume) as a Juice bloc.