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에서 이벤트를 수신받아 이전과 동일하게 로컬 저장소에 상태를 저장하여 저장된 상태 데이터를 화면에 보여주는 방법으로 개발을 하였다.
State Management는 Provider를 사용하였고, PlatformChannel은 제 블로그에서는 처음 다뤄보는 방법인 MessageChannel로 플랫폼간 통신을 하도록 하겠다.
MessageChannel은 간단한 문자열을 플랫폼간 송수신하기에 간편하게 사용할 수 있어 EventChannel이나 MethodChannel을 사용하지 않았다.
dependencies:
proivder: ^6.0.4
shared_preferences: ^2.0.17
전체적인 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에서 앱 상태 문자열을 로컬 저장소로 받아오기 위한 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);
}
IOS의 라이프 사이클에 대해서 간단하게 살펴보자. FLutter의 라이프 사이클은 4가지의 상태를 통해서 확인해 볼 수 있다.
IOS에서는 NotRunning, Inactive, Active, Background, Suspended 이렇게 5가지의 상태를 제공하고 있는데, 5가지의 상태를 전부 사용하지 않을 것이고 앱 종료, 앱 백그라운드, 앱 실행 중 이렇게 3가지의 상태에 대해서만 이벤트를 등록하여 사용할 예정이다.
자세한 내용은 Swift 코드와 함께 살펴보자.
먼저 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)
}
https://github.com/boglbbogl/flutter_velog_sample/blob/main/ios/Runner/AppDelegate.swift
여기까지해서 IOS에서 Swift 코드를 통해서 앱 상태를 수신받아 Message Channel로 Flutter와 통신하여 Flutter에서 API를 정상적으로 호출하는 것까지 확인해 보았다.
다음 글에서는 Android에서 처리하는 방법에 대해서도 확인해 보도록 하곘다.