ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Flutter] Flutter의 onStart, onStop, onResume, onPause
    Flutter 2024. 4. 1. 00:17
    반응형

    Flutter의 생명 주기

    Flutter의 생명 주기 (lifecycle)은 Android의 Activity의 생명 주기와 다소 상이하게 동작합니다. 당연한 것이지만 Flutter의 위젯은 Android의 View 기반이므로 Activity의 하위 컴포넌트이니까 Activity보다 늦게 나타나고 먼저 사라지게 됩니다. 그렇기 때문에 Activity가 create 돼서야 비로소 위젯이 그려질 수 있습니다. 
     
    Flutter의 위젯에는 StatelessWidgetStatefulWidget이 존재하는데, StatelessWidget일 경우엔 독자적인 생명 주기가 존재하지는 않고 부모 위젯이 사라지면 같이 사라지게 됩니다. 이에 반해 StatefulWidget은 독자적인 상태와 생명 주기를 가지고 특정 작업이나 조건이 들어오면 혼자서 사라질 수 있습니다.
     
    StatelessWidget은 다음과 같은 메서드를 가지고 생명 주기를 표현하게 됩니다.

    출처: Flutter 공식 Youtube 중 InheritedWidgets 일부

     
    1. construction (createState())
    위젯이 만들어질 때 상태를 함께 생성하게 됩니다.
     
    2. initState()
    위젯의 상태를 야기하는 데이터를 초기화합니다. 생성 후 최초 1번만 실행되기 때문에 순전히 초기화용으로 사용하면 됩니다.
     
    3. didChangeDependencies()
    부모 위젯이나 InheritedWidget 등의 종속성이 바뀔 때 호출되는데, initState 직후에 한 번 이상 호출될 수 있습니다. (생성 전에 의존성이 변경될 때 다시 호출될 수 있음)
     
    4. didUpdateWidget()
    부모 위젯이 rebuild 될 때 등 상태가 변경될 때 호출됩니다. (예를 들어 ListView의 껍데기에 의해 Item 자식 위젯이 바뀔 수 있는 등) 그래서 아이템을 굳이 다시 바꾸고 싶지 않으면 여기서 현재 위젯과 이전 위젯을 비교해서 그릴 지 말 지 결정할 수 있습니다.
     
    5. build()
    상태가 변경될 때마다 호출됩니다. 여기다 상태 변경 로직을 넣으면 재귀적으로 계속 호출될 수 있으니 주의해야 합니다.
     
    6. setState()
    상태 변경을 강제로 호출해 줍니다. 위젯은 setState가 호출되면 위젯을 다시 렌더링 합니다.
     
    7. deactivate()
    위젯 트리에서 빠지게 될 쯤에 호출됩니다. 즉 화면에서 사라질 때 호출되어 다 쓴 데이터를 정리하는 작업을 합니다.
     
    8. dispose()
    위젯이 파괴될 때 호출됩니다. 만약 해당 위젯이 controller나 listener를 생성해서 가지고 있었다면 그 친구들도 dispose 해주어야 합니다. 안 그러면 메모리 누수가 발생할 수 있습니다.
     
    위의 메서드들을 통해서 위젯의 상태와 화면이 어떻게 구성되어 있는지는 어지간하면 전부 알아낼 수 있습니다.
    그런데 가령 안드로이드의 액티비티처럼 잠깐 화면에서 안 보이거나, 상태바에 의해 가려지거나, 다른 앱에 갔다 오거나 등을 명확하게 표현하고 싶을 수 있습니다. 주로 백그라운드에서 포그라운드로 돌아올 때에 대한 감지를 원하는 경우가 많은데 이 경우는 다른 방법으로 명확하게 알아낼 수 있습니다. 

     

     

    반응형


     

    Flutter의 WidgetsBindingObserver와 AppLifecycleListener

    State에 WidgetsBindingObserver라는 녀석을 다는 방법과 AppLifecycleListener를 구성해서 사용하는 방법이 있습니다. 이 두 친구들은 앱이 현재 사용자에게 실질적으로 보이고 있는지, 잠깐 다른 작업을 하러 갔는 지를 확인할 수 있습니다. 일단 방법은 아래와 같습니다.
     
    WidgetsBindingObserver
    WidgetsBindingObserver는 State에 부착시킨 후에 listener를 initState에서 등록하고 dispose에서 remove 해서 등록해 두면  didChangeAppLifecycleState에서 화면에 대한 상태를 자동으로 호출해 주는 mixin입니다. 기본적인 상태는 아래와 같은데, 여기서 Flutter 3.13 버전 이상이냐 혹은 이하냐에 따라 hidden이라는 타입이 하나 생겼습니다.
     
    1. AppLifecycleState.resumed
    화면에 보이고 사용자의 입력에 응답할 때 호출됩니다.
     
    2. AppLifecycleState.paused
    화면에 보이지 않고 사용자의 입력을 받을 수 없고 백그라운드로 갈 때 호출됩니다.
     
    3. AppLifecycleState.inactive
    화면에 보이지 않으며 사용자의 입력을 받을 수 없을 때 호출됩니다.
     
    4. AppLifecycleState.detached
    Flutter의 Engine이 앱을 실행시키고는 있는데 View와는 분리된 상태로 View 없이 앱이 실행됩니다.
     
    5. AppLifecycleState.hidden
    Flutter 3.13부터 생긴 타입이며, 앱이 화면에 보이지 않기 시작하고 입력을 받지 못하다가 완전히 가려지기 전에 호출됩니다. (백그라운드에 가기 전에 호출되는 것으로 보입니다.)
     

    class WidgetsBindingObserverWidget extends StatefulWidget {
      const WidgetsBindingObserverWidget({Key? key}) : super(key: key);
    
      @override
      State<WidgetsBindingObserverWidget> createState() => _WidgetsBindingObserverWidgetState();
    }
    
    class _WidgetsBindingObserverWidgetState extends State<WidgetsBindingObserverWidget> with WidgetsBindingObserver {
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance.addObserver(this);
      }
    
      @override
      void dispose() {
        WidgetsBinding.instance.removeObserver(this);
        super.dispose();
      }
    
      @override
      void didChangeAppLifecycleState(AppLifecycleState state) {
        super.didChangeAppLifecycleState(state);
        // 여기에서 WidgetsBindingObserver가 감지한 상태에 따라 원하는 처리를 해주면 됩니다.
        if (state == AppLifecycleState.resumed) {
          // 앱이 화면에 보여지고 사용자의 입력에 응답할 때 호출됩니다.
        } else if (state == AppLifecycleState.paused) {
          // 앱이 화면에 보여지지 않으며 사용자의 입력을 받을 수 없고 백그라운드에서 동작합니다.
        } else if (state == AppLifecycleState.inactive) {
          // 위젯이 화면에서 보여지지 않으며 사용자의 입력을 받을 수 없는 상태입니다.
        } else if (state == AppLifecycleState.detached) {
          // Flutter Engine이 앱을 그리고는 있는데 View는 분리됩니다.
          // 즉 View가 없는 상태에서 앱이 실행됩니다.
        } else if (state == AppLifecycleState.hidden) {
          // Flutter 3.13 버전 이상부터 생긴 타입입니다.
        }
    
        // 앱이 포그라운드 -> 백그라운드일 때
        // inactive -> hidden -> paused 상태로 전환
    
        // 앱이 백그라운드 -> 포그라운드일 때
        // hidden -> inactive -> resumed 상태로 전환
    
        // 웹뷰의 시스템 알럿이 발생할 때
        // inactive -> resumed 상태로 전환
    
        print('[keykat] $state');
      }
    
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }

     
    그런데 WidgetsBindingObserver의 문제가 하나 있었는데, Android의 onStart (onRestart)를 잡아낼 수 없다는 것입니다. 액티비티가 백그라운드에서 올라올 때 한 번 호출되는 것을 캐치하고 싶은데, AppLifecycleState.resumed는 해당 상황뿐만 아니라 사용자의 입력에 의해 호출될 수 있는 상태이다 보니, 이게 백그라운드에서 포그라운드로 올라와서 호출된 것인지, 아니면 시스템 alert을 닫았을 때 호출되는 것인지 알 수가 없었습니다.
     
    간혹 WebView를 사용하다 보면 웹의 시스템 alert이 발생하게 되는데, 이 alert이 발생할 때 AppLifecycleState.inactive가 발생하고 (해당 옵저버를 달아놓은 위젯이 알림 창 때문에 사용자의 입력을 잠시동안 잃기 때문) 알림 창을 닫으면 AppLifecycleState.resumed가 발동합니다. 때문에 백그라운드에서 포그라운드로 올라왔을 때에만 동작하고 싶었던 코드를 resumed 상태일 때 구현해 놓았는데, 정작 WebView의 알림 창과 상호작용을 할 때도 발동했었던 경험이 있습니다.
     
    따라서 Android에서 onStart와 onRestart를 잡아내기 위해선 Channeling을 통해 FlutterActivity의 onStart와 onRestart 호출 시에 listener를 달아서 해결할 수밖에 없었습니다. 이러한 문제를 Flutter 팀은 3.13 버전 이상부터 AppLifecycleListener를 이용해서 해결해 주었습니다.
     
    AppLifecycleListener
    AppLifecycleListener는 Flutter 3.13 버전 이상에 새로 생긴 클래스입니다. WidgetsBindingObserver의 역할이 비교적 간단한 상태를 표시해주고 있었다면, AppLifecycleListener는 현재 AppLifecycleState와 함께 실질적으로 앱과 사용자 간의 인터렉션에서 앱의 상태에 따라 callback 함수도 같이 호출해 줍니다. 아래는 callback과 그 callback에 의해 변경되는 state 상태를 보여주는 그림입니다.
     

    출처: https://api.flutter.dev/flutter/widgets/AppLifecycleListener-class.html

     
     

    class AppLifecycleListenerWidget extends StatefulWidget {
      const AppLifecycleListenerWidget({Key? key}) : super(key: key);
    
      @override
      State<AppLifecycleListenerWidget> createState() => _AppLifecycleListenerWidgetState();
    }
    
    class _AppLifecycleListenerWidgetState extends State<AppLifecycleListenerWidget> {
      late final AppLifecycleListener _listener;
      late AppLifecycleState? _state;
    
      @override
      void initState() {
        super.initState();
        _state = SchedulerBinding.instance.lifecycleState;
    
        _listener = AppLifecycleListener(
          onShow: () {
            // 앱이 보여질 때 호출됩니다.
            print('[keykat] onShow');
          },
          onResume: () {
            print('[keykat] onResume');
          },
          onHide: () {
            print('[keykat] onHide');
          },
          onInactive: () {
            print('[keykat] onInactive');
          },
          onPause: () {
            print('[keykat] onPause');
          },
          onDetach: () {
            print('[keykat] onDetach');
          },
          onRestart: () {
            print('[keykat] onRestart');
          },
    
          // foreground -> background
          // callback: onInactive -> onHide -> onPause
          // state: AppLifecycleState.inactive -> AppLifecycleState.hidden -> AppLifecycleState.paused
    
          // background -> foreground
          // callback: onRestart -> onShow -> onResume
          // state: AppLifecycleState.hidden -> AppLifecycleState.inactive -> AppLifecycleState.resumed
          
          // webview system alert shown
          // callback: onInactive
          // state: AppLifecycleState.inactive
    
          // webview system alert hidden
          // callback: onResume
          // state: AppLifecycleState.resumed
          onStateChange: (state) {
            print('[keykat] $state');
          },
        );
      }
    
      @override
      void dispose() {
        _listener.dispose();
        super.dispose();
      }
    
    
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }

     
    여기서 위의 주석을 보면 onRestart라는 기능을 추가해 주었습니다. WidgetsBindingObserver에서 크로스 플랫폼 별로 명확하게 대응하지 못했던 것을 새로운 state와 callback 타입을 추가해서 대응할 수 있게 만든 것 같습니다. 덕분에 더 이상 플랫폼 별 Channeling을 할 필요가 없게 되었습니다.
     

    요약하자면,

    1. Flutter 3.13 버전 아래에서는 WidgetsBindingObserver를 사용하고, onStart에 대응하기 위해선 Channeling을 해야 된다.
    2. Flutter 3.13 버전 이상에서는 AppLifecycleListener로 상태를 적절하게 활용할 수 있다.
    반응형
Designed and Written by keykat.