Lifecycle(앱 상태) 이벤트 4편

Lifecycle(앱 상태) 이벤트 1편
Lifecycle(앱 상태) 이벤트 2편
Lifecycle(앱 상태) 이벤트 3편
Lifecycle(앱 상태) 이벤트 5편
JSONPlaceholder

provider | Flutter Package
shared_preferences | Flutter Package

App Life Cycle에 대하여 정리해 놓은 인트로 참고하세요 !
App Lifecycle Intro

이전 글까지 Flutter에서 Lifecycle(앱 상태) 이벤트를 생성하고 수신하는 방법에 대해서 살펴보았다. 이전에도 언급했듯이 Flutter에서는 앱 종료시 상태 이벤트가 제대로 수신되지 않는다는 이슈를 확인했다.

그럼 앱이 종료 됬을 때 이벤트를 얻을 수 있는 방법이 아예 없는 걸까 ?
아니다. 지금까지 확인한 방법이 하나 있다. 바로 Native에서 앱 종료 상태를 받아오는 방법이다.

Native에서 앱 상태 이벤트를 수신받아 Platform Channel을 통해서 Flutter로 이벤트를 호출해주면 된다. 아니면 아예 필요한 기능을 Native 코드에서 처리하여도 된다.

하지만 Native에서 앱 라이플사이클(상태)을 처리하는 데에도 문제가 있는데, 앱 종료 상태 이벤트가 호출되고 처리 시간이 1초가 되지 않기에 기능을 작동시키기에는 매우 짧은 시간만 허용해 준다. 추가로 백그라운드 프로세스마저도 닫히게 된다는 문제가 있다.

우선 Native에서 앱 상태를 얻는 코드를 작성해보고, Platform channel로 Flutter에 넘겨주는 방법과 밀리세컨안에서 기능을 작동시키는 방법에 대해서 차근차근 알아보도록 하자.

4편에서는 IOS에서 Swift 코드를 통해서 처리하는 방법에 대해서 살펴보고, 다음 5편에서 Android에서 앱 상태를 처리하는 방법을 Kotlin 코드를 통해서 작성하도록 하겠다.

Flutter

Flutter에서 이벤트를 수신받아 이전과 동일하게 로컬 저장소에 상태를 저장하여 저장된 상태 데이터를 화면에 보여주는 방법으로 개발을 하였다.

State Management는 Provider를 사용하였고, PlatformChannel은 제 블로그에서는 처음 다뤄보는 방법인 MessageChannel로 플랫폼간 통신을 하도록 하겠다.

MessageChannel은 간단한 문자열을 플랫폼간 송수신하기에 간편하게 사용할 수 있어 EventChannel이나 MethodChannel을 사용하지 않았다.

dependencies

dependencies:
	proivder: ^6.0.4
	shared_preferences: ^2.0.17

UI

전체적인 UI 구조는 이전 글에서 살펴본 구조와 동일한 구조이다.

네이티브와 Flutte의 Platform Channel 방법 중 MessageChannel을 사용하여 개발을 하였고, Message Channel을 등록하기 위해 appLifeCycleState 채널을 생성하였다.

Message Channel은 간단한 문자열을 플랫폼 간의 통신을 사용할 때 간단하게 구성할 수 있는 Platform Channel이다.

Provider를 사용을 위해 Consumer 빌드를 생성하여 구성 하였고, setMessageHandler, started 기능은 아래에서 살펴보도록 하겠다.

context.read<LifeCycleNativeProvider>().started();
    const BasicMessageChannel<String> appLifeCycleState =
        BasicMessageChannel<String>('appLifeCycle', StringCodec());
    return Consumer<LifeCycleNativeProvider>(builder: (context, state, child) {
      appLifeCycleState.setMessageHandler(state.appLifeCycleChecked);
      return Scaffold(
        appBar: appBar(title: "Life Cycle With Native"),
        body: lifeCycleUIListView(data: state.lifeCycle, context: context),
      );
    });
ListView lifeCycleUIListView({
  required List<String> data,
  required BuildContext context,
}) {
  return ListView(
    children: [
      const SizedBox(height: 12),
      ...data.map(
        (e) => Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              SizedBox(
                  width: MediaQuery.of(context).size.width * 0.2,
                  child: Text(
                    e.split("/")[0],
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      color: e.split("/")[0] == "inactive"
                          ? Colors.amber
                          : e.split("/")[0] == "stop"
                              ? Colors.amber.shade200
                              : e.split("/")[0] == "detached"
                                  ? Colors.deepOrange
                                  : e.split("/")[0] == "restart"
                                      ? Colors.blue.shade200
                                      : e.split("/")[0] == "resumed"
                                          ? Colors.blue
                                          : Colors.green,
                    ),
                  )),
              const SizedBox(width: 12),
              Text(
                e.split("/")[1],
                style: const TextStyle(
                  color: Color.fromRGBO(195, 195, 195, 1),
                ),
              ),
            ],
          ),
        ),
      )
    ],
  );
}

Provider

Provider에서 앱 상태 문자열을 로컬 저장소로 받아오기 위한 lifeCycle 변수를 선언해주었다.
lifeCycleKey는 로컬 저장소에 사용할 고유 키이다.

  List<String> lifeCycle = [];
  final String _lifeCycleKey = "APP_LIFE_CYCLE_CHECK_WITH_NATIVE";

앱의 상태를 Native로 부터 수신 받아 처리하는 부분의 코드이다. MessageChannel을 통해 문자열을 수신 받아 로컬 저장소에 저장해주면 된다.

Future<String> appLifeCycleChecked(String? message) async {
    if (message != null) {
      switch (message) {
        case "lifeCycleStateWithDetached":
          _setLocalStorage("Detached");
          break;
        case "lifeCycleStateWithResumed":
          _setLocalStorage("Resumed");
          _getLocalStorage();
          break;
        case "lifeCycleStateWithInactive":
          _setLocalStorage("Inactive");
          break;
        default:
      }
    }
    return message!;
  }

로컬 저장소에 저장해둔 문자열을 가져오기 위한 코드이다.

Future<void> _getLocalStorage() async {
    SharedPreferences _pref = await SharedPreferences.getInstance();
    List<String> _list = _pref.getStringList(_lifeCycleKey) ?? [];
    lifeCycle = _list;
    notifyListeners();
  }

각 상태에 대해서 문자열을 로컬 저장소에 저장해두기 위한 코드이다.

  Future<void> _setLocalStorage(String value) async {
    String _saveData = "$value/${DateTime.now().toString().substring(0, 19)}";
    SharedPreferences _pref = await SharedPreferences.getInstance();
    List<String> _list = _pref.getStringList(_lifeCycleKey) ?? [];
    _list.add(_saveData);
    notifyListeners();
    _pref.setStringList(_lifeCycleKey, _list);
  }

Swift

IOS의 라이프 사이클에 대해서 간단하게 살펴보자. FLutter의 라이프 사이클은 4가지의 상태를 통해서 확인해 볼 수 있다.

IOS에서는 NotRunning, Inactive, Active, Background, Suspended 이렇게 5가지의 상태를 제공하고 있는데, 5가지의 상태를 전부 사용하지 않을 것이고 앱 종료, 앱 백그라운드, 앱 실행 중 이렇게 3가지의 상태에 대해서만 이벤트를 등록하여 사용할 예정이다.

자세한 내용은 Swift 코드와 함께 살펴보자.

AppDelegate

먼저 Flutter와 Swift 코드간의 플랫폼 채널 연결을 위해 MessageChannel을 생성하도록 하자.

@objc class AppDelegate: FlutterAppDelegate {

    var appLifeCycle: FlutterBasicMessageChannel!

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    appLifeCycle = FlutterBasicMessageChannel(
            name: "appLifeCycle",
            binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger,
            codec: FlutterStringCodec.sharedInstance())

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  ...lifeCycle

}

앱 종료시 IOS의 Delegate가 파괴될 때 호출되는 부분의 코드이다. Flutter의 detached 상태와 동일하다고 보면된다.

override func applicationWillTerminate(_ application: UIApplication) {
        appLifeCycle.sendMessage("lifeCycleStateWithDetached")
    }

앱의 백그라운드 또는 비활성화된 후 앱이 다시 활성화 되었을 때 호출 되는 부분으로 Flutter의 resumed 상태와 동일하다.

override func applicationWillEnterForeground(_ application: UIApplication) {
        appLifeCycle.sendMessage("lifeCycleStateWithResumed")
    }

마지막으로 앱 백그라운드 또는 비활성화, 뷰에서 사라질 때에 호출되는 상태 이벤트이다.

override func applicationDidEnterBackground(_ application: UIApplication) {
        appLifeCycle.sendMessage("lifeCycleStateWithInactive")
    }

자 여기까지 해서 테스트를 해보면 앱의 라이플 사이클 변경에 따라 Native에서 수신받은 이벤트가 Message Channel을 통해서 Flutter에 호출되어 로컬 저장소에 상태 값이 저장되는 것을 확인할 수 있다.

이제 앱을 종료 해보고, 다시 진입해 보면 종료 상태 값이 로컬 저장소에 저장이 되어있지 않다.

앱 종료 상태에서 로그를 출력할 수 있도록 코드를 추가하여 앱을 종료해보자. 드디어 로그가 출력되는 것을 볼 수 있다.
하지만 여전히 로컬 저장소에 저장이 되지 않고 있다. 이번엔 API를 한 번 호출해 보자.

Future<String> appLifeCycleChecked(String? message) async {
    if (message != null) {
      switch (message) {
        case "lifeCycleStateWithDetached":
        	logger.e("Detached Call");
          _setLocalStorage("Detached");
          break;
       ...
        default:
      }
    }
    return message!;
  }

앱 종료 이벤트에서 API를 호출하는 기능을 추가하자.

Future<String> appLifeCycleChecked(String? message) async {
    if (message != null) {
      switch (message) {
        case "lifeCycleStateWithDetached":
          _detachedStateToApi();
          break;
       ...
        default:
      }
    }
    return message!;
  }

무료로 API 호출을 할 수 있는 기능을 제공하고 있는 JSONPlaceHolder를 사용하여 API를 호출해 보자. 결과는 역시나 아무 로그도 찍히지 않고 있다.

위에서도 언급했던 네이티브 이슈인 applicationWillTerminate가 수신될 때에 밀리세컨 단위의 시간만 허용하기 때문에 API를 호출할 수 없었던 것이다.

위의 이슈를 어떻게 하면 해결할 수 있을까 ?

 Future<void> _detachedStateToApi() async {
    final uri = Uri.parse("https://jsonplaceholder.typicode.com/users");
    final response = await http.get(uri);
    if (response.statusCode == 200) {
      logger.d(response.body);
      logger.d(response.statusCode);
    }
  }

아래에 sleep 기능을 사용하여 쓰레드를 일시적으로 멈추어보자. 2초 정도 쓰레드를 멈추고 Flutter 에서 API를 다시 호출해보면 이제 정상적으로 API가 호출된 것을 확인할 수 있다.

override func applicationWillTerminate(_ application: UIApplication) {
        appLifeCycle.sendMessage("lifeCycleStateWithDetached")
        sleep(2)

    }

Result

Git

Flutter

https://github.com/boglbbogl/flutter_velog_sample/blob/main/lib/life_cycle/life_cycle_native_provider.dart

Swift

https://github.com/boglbbogl/flutter_velog_sample/blob/main/ios/Runner/AppDelegate.swift

마무리

여기까지해서 IOS에서 Swift 코드를 통해서 앱 상태를 수신받아 Message Channel로 Flutter와 통신하여 Flutter에서 API를 정상적으로 호출하는 것까지 확인해 보았다.

다음 글에서는 Android에서 처리하는 방법에 대해서도 확인해 보도록 하곘다.

profile
Flutter Developer

0개의 댓글