패키지, 저장소 및 앱 카테고리 검색

패키지 상세v1.0.0

lifecycle

위젯 라이프사이클에 쉽게 훅을 추가할 수 있는 플러터 패키지입니다.

AndroidiOSWebmacOSWindowsLinux
30일 다운로드
4.1천
좋아요
81
Pub 점수
160

패키지 설치

flutter pub add lifecycle

패키지 개요

공개 정보를 바탕으로 생성한 선택 가이드입니다.

라이프사이클 패키지는 플러터에서 위젯 라이프사이클 이벤트에 훅을 추가하는 간단한 방법을 제공하여 초기화, 해제 및 재구성과 같은 특정 단계에서 코드를 실행할 수 있게 합니다. 복잡한 보일러플레이트 없이 깔끔하고 선언적인 라이프사이클 콜백을 제공함으로써 상태 관리를 강화합니다. 위젯이 UI 트리에 존재할 때 관련된 부작용, 구독 또는 정리 작업을 관리하는 데 이상적입니다. 이 패키지는 Android, iOS, 웹, macOS, Windows, Linux 등 모든 주요 플랫폼을 지원합니다.

사용 사례

  • 위젯 수명 주기 동안 구독 관리
  • 위젯 생성 및 파괴 시 코드 실행
  • 상태가 있는 위젯에서 부작용 처리
  • 정리 로직 간소화
  • 반응형 상태 관리와의 통합
  • 메모리 누수 방지

주요 기능

  • 선언적 라이프사이클 훅
  • 최소한의 보일러플레이트
  • 크로스 플랫폼 지원
  • 기존 위젯과의 간편한 통합
  • init, dispose 및 rebuild 이벤트를 지원합니다
  • 경량이며 종속성 없음

적합한 대상

  • 상태를 가진 위젯을 사용하는 개발자
  • 기능형 스타일의 플러터 패턴을 채택하는 팀
  • 세밀한 라이프사이클 제어가 필요한 프로젝트
  • StatefulWidget의 대안을 찾는 사람
  • 자주 위젯을 생성하고 삭제하는 앱
  • 깨끗하고 유지보수 가능한 코드에 집중하는 빌더

고려 사항

  • 간단한 위젯에는 필요하지 않습니다
  • 고주파도 위젯 업데이트에서 오버헤드가 발생할 수 있습니다
  • Flutter의 위젯 라이프사이클에 대한 이해 필요
  • 생명주기별 사용 사례에 한정
  • 완전한 상태 관리 솔루션을 대체하지 않습니다
  • 다른 상태 처리 방법과 함께 사용하는 것이 가장 좋습니다

원본 README

연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.

Pub Version pub points likes

English | 简体中文

lifecycle

Composable lifecycle state for Flutter widgets.

lifecycle combines app state, Navigator routes, paged containers, and scroll viewports into one immutable LifecycleSnapshot. A descendant can only be active when every containing scope is active:

App → Navigator route → Page/Tab → Viewport item → Widget

There is no global observer and no event bus. Each Navigator owns an explicit controller, nested scopes compose automatically, and callbacks receive both the resulting snapshot and the cause of the transition.

Lifecycle model

The current state is represented by:

LifecycleSnapshot.active(
  visibleFraction: 1,
  appState: AppLifecycleState.resumed,
)

Phases are detached, hidden, visible, active, and disposed. A single phase is the single source of truth; attached, visible, and active are derived getters. A single state change produces events in deterministic order:

created → appeared → activated → deactivated → disappeared → disposed

Only events that apply to a transition are emitted. For example, hiding an active widget emits deactivated and then disappeared.

LifecycleTransition.cause identifies whether the change came from the app, a route, a back gesture, page selection, a viewport, the widget tree, or a custom boundary.

Installation

dependencies:
  lifecycle: ^1.0.0
import 'package:lifecycle/lifecycle.dart';

App and Navigator setup

Create one NavigatorLifecycleController per Navigator. The controller owns its observer; dispose the controller where you create it.

class AppState extends State<App> {
  final navigation = NavigatorLifecycleController();

  @override
  Widget build(BuildContext context) {
    return LifecycleApp(
      child: MaterialApp(
        navigatorObservers: [navigation.observer],
        builder: (context, child) => NavigatorLifecycleScope(
          controller: navigation,
          child: child!,
        ),
        home: const HomeScreen(),
      ),
    );
  }

  @override
  void dispose() {
    navigation.dispose();
    super.dispose();
  }
}

Use a separate controller, observer, and scope for each nested Navigator. Both imperative routes and the Navigator pages API are supported. Opaque routes hide the route below; non-opaque routes such as dialogs keep it visible but inactive. Interactive back gestures expose the previous route as visible.

Routes can also be inspected or removed through the controller:

final route = navigation.routeNamed('/checkout');
final routes = navigation.routes;

if (route != null) {
  final lifecycle = navigation.lifecycleFor(route);
  debugPrint('checkout: ${lifecycle?.phase}');
  navigation.removeRoute(route);
}

Listen from a widget

For a stateless subtree, use LifecycleListener:

LifecycleListener(
  onTransition: (transition) {
    debugPrint('${transition.previous.phase} → ${transition.current.phase}');
  },
  onEvent: (event, transition) {
    debugPrint('${event.name} (${transition.cause.name})');
  },
  child: const Content(),
)

For a State, use LifecycleStateMixin:

class ArticleState extends State<Article> with LifecycleStateMixin<Article> {
  @override
  void onLifecycleEvent(
    LifecycleEvent event,
    LifecycleTransition transition,
  ) {
    if (event == LifecycleEvent.activated) {
      refreshArticle();
    }
  }

  @override
  Widget build(BuildContext context) => const ArticleView();
}

LifecycleBuilder rebuilds when any snapshot field changes, including visibleFraction:

LifecycleBuilder(
  builder: (context, lifecycle) {
    return Text(lifecycle.phase.name);
  },
)

Custom boundaries

Use LifecycleBoundary to compose domain state with the surrounding scope. It can restrict a descendant, but it cannot make a descendant more active than its parent.

LifecycleBoundary(
  constraint: !panelIsOpen || animation.value <= 0
      ? const LifecycleConstraint.hidden()
      : panelHasFocus
          ? LifecycleConstraint.active(visibleFraction: animation.value)
          : LifecycleConstraint.visible(visibleFraction: animation.value),
  child: const Panel(),
)

PageView

LifecyclePageView owns page scopes and forwards the usual PageView options. The selected page becomes active only after scrolling settles. During a drag, visible pages expose a fractional visibility and remain inactive.

final controller = PageController(initialPage: 1);

LifecyclePageView(
  controller: controller,
  onPageTransition: (index, transition) {
    debugPrint('page $index: ${transition.events}');
  },
  children: const [
    FeedPage(key: ValueKey('feed')),
    SearchPage(key: ValueKey('search')),
  ],
)

The builder constructor supports large and reorderable data sets. Supply a stable ID and its reverse lookup when order can change:

LifecyclePageView.builder(
  controller: controller,
  itemCount: items.length,
  pageIdBuilder: (index) => items[index].id,
  findPageIndex: (id) => items.indexWhere((item) => item.id == id),
  itemBuilder: (context, index) => ItemPage(
    key: ValueKey(items[index].id),
    item: items[index],
  ),
)

The widget deliberately does not change Flutter's keep-alive policy. A lazy page that is disposed emits disposed; wrap page content with AutomaticKeepAliveClientMixin when its State must survive off-screen.

TabBarView

LifecycleTabBarView(
  controller: tabController,
  onTabTransition: (index, transition) {
    debugPrint('tab $index: ${transition.events}');
  },
  children: const [OverviewTab(), ActivityTab()],
)

LifecycleTabBarView follows both animated taps and horizontal swipes. A tab is active only when the TabController is settled.

ListView, GridView, and CustomScrollView

Place ViewportLifecycleItem below the nearest Scrollable. Visibility is calculated from the item's two-dimensional area inside that viewport, so vertical lists, horizontal lists, grids, and slivers share the same behavior.

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ViewportLifecycleItem(
    visibleThreshold: 0.25,
    activeThreshold: 0.8,
    activationPolicy:
        ViewportLifecycleActivationPolicy.whenScrollSettles,
    onTransition: (transition) {
      debugPrint('item $index: ${transition.current.visibleFraction}');
    },
    child: ItemTile(item: items[index]),
  ),
)

An item is visible when its fraction reaches visibleThreshold. By default it can become active only after scrolling settles and its fraction reaches activeThreshold; use ViewportLifecycleActivationPolicy.immediate to retain activation while scrolling. If activeThreshold is omitted, it uses the visible threshold. Reported fractions are stabilized to one-percent increments by default; set visibleFractionGranularity to zero for exact measurements. The containing app, route, and page states still apply.

Direct controller use

Most apps only need widgets, but LifecycleController is public for custom containers. Child state is composed with its parent, listeners are safe to add/remove during delivery, and reentrant updates are queued deterministically.

final parent = LifecycleController()..attach();
final child = LifecycleController(
  constraint: const LifecycleConstraint.hidden(),
);

void onChildChanged() {
  final transition = child.lastTransition!;
  debugPrint('${transition.previous.phase} -> ${transition.current.phase}');
}

child
  ..addListener(onChildChanged)
  ..attach(parent: parent);

child.updateLocal(
  constraint: const LifecycleConstraint.active(),
  cause: LifecycleCause.custom,
);

final subtree = LifecycleScope(
  controller: child,
  child: const CustomContainerContent(),
);

LifecycleController uses the standard ChangeNotifier channel for both snapshot and transition changes. During a listener callback, value is the latest snapshot and lastTransition describes the atomic change that produced it. Remember to call removeListener when the listener can outlive the controller.

See the example application and the tests for complete runnable compositions.

1.0 migration

Version 1.0 is a deliberate API redesign. The global defaultLifecycleObserver, legacy wrapper classes, and dispatch/subscribe mixins were removed. Replace them with explicit LifecycleApp and NavigatorLifecycleController setup, then use LifecycleListener, LifecycleStateMixin, LifecyclePageView, LifecycleTabBarView, and ViewportLifecycleItem.

카테고리

소스 저장소

이 패키지를 사용하는 프로젝트

아직 데이터가 없습니다.