Project/Ear Alarm

Attempting to launch an unregistered ActivityResultLauncher with contract

oldogz 2025. 5. 24. 03:48

Ear Alarm 서비스를 1.5.3 버전 업데이트를 한 이후, Firebase Crashlytics에서 InAppUpdate와 관련된 오류를 확인할 수 있었습니다.

 

Fatal Exception: java.lang.IllegalStateException
Attempting to launch an unregistered ActivityResultLauncher with contract androidx.activity.result.contract.ActivityResultContracts$StartIntentSenderForResult@b26687f and input androidx.activity.result.IntentSenderRequest@46b604c. You must ensure the ActivityResultLauncher is registered before calling launch().

 

해당 오류는 일반적으로 ActivityResultLauncher가 제대로 등록되지 않은 상태에서 launch() 메서드를 호출했을 때 발생합니다.

 

에러 분석

첫 화면에서 InAppUpdate를 확인하고 요청하는 로직으로, 전혀 문제가 없을 줄 알았지만, ActivityResultLauncher에 대해서 예외가 발생할 수 있다는 것을 알게 되었습니다.

 

 

유연한 업데이트의 경우 사용자가 업데이트를 거절하고 앱을 계속 사용할 수 있습니다. 오류가 발생한 로그를 살펴보면

HomeScreen에 진입하고, 빠르게 MeasureScreen으로 전환되며 오류가 발생한 것을 확인할 수 있었습니다.

 

기존 화면 구성

앱의 전체 화면은 간단한 방식으로 구성되어 있습니다.

 

TimerScreen을 StartDestination으로 내비게이션이 진행되고, TimerScreen에서는 현재 동작 중인 타이머가 있는지 판단하여 HomeScreen과 MeasureScreen을 분기합니다.

 

중간에 Loading Screen이 없이 초기값이 HomeScreen으로 지정되어 있었기 때문에, 타이머가 동작하고 있는 상태라면 HomeScreen에서 시작하여 빠르게 MeasureScreen으로 전환됩니다.

 

문제는 인앱 업데이트의 확인 및 요청의 로직이 HomeScreen에 존재하였습니다.

 

val appUpdateFlexibleResultLauncher = rememberLauncherForActivityResult(
    ActivityResultContracts.StartIntentSenderForResult(),
) { result ->
    if (result.resultCode != Activity.RESULT_OK) {
        setRejectFlexibleUpdateDate()
    }
}

 

문제가 발생한 ActivityResultLauncher는 rememberLauncherForActivityResult 메서드로 해당 컴포저블이 사라지면 함께 사라지게 됩니다.

 

또한 인앱 업데이트 요청의 경우 비동기로 동작하기 때문에, 업데이트 요청을 시작하고 컴포저블이 사라지면서 ActivityResultLauncher가 없어진 후 최종적으로 업데이트 요청에서 launch를 수행할 때 오류가 발생할 수 있다는 것을 알게 되었습니다.

 

또한 유연한 업데이트의 경우, 주기를 짧게 설정해놓았기 때문에 발견할 수 있는 오류기도 하였습니다.

유연한 업데이트의 거절 주기를 따로 저장하지 않고 ViewModel에만 저장해 두었기 때문에 앱을 종료하고 다시 실행하면 다시 유연한 업데이트 요청이 수행되도록 했었습니다.

 

주기가 짧았기 때문에 타이머가 종료되지 않은 시점에서 다시 앱을 실행시켰을 때 오류가 발생하여 문제를 발견할 수 있었던 것 같습니다.

 

유연한 업데이트에서 발생할 수 있는 시나리오 및 재현

1. 유연한 업데이트 가능 상태 확인 및 요청

2. 사용자가 거절 → 앱 계속 사용 (앱이 켜져있는 동안 더 이상 요청하지 않음)

3. 타이머 알람 설정

4. 앱 종료 (유연한 업데이트 거절 주기 초기화)

5. 앱 실행

6. HomeScreen 진입

7. startUpdateFlowForResult()를 통해 업데이트 요청 Flow 시작

8. 설정된 타이머 있을 경우 바로 TimerScreen 전환 (HomeScreen 컴포저블 종료)

9. ActivityResultLauncher 사라짐

10. startUpdateFlowForResult의 Flow 내에서 ActivityResultLauncher 등록 안됨

11. 오류 발생

 

다음과 같은 순서로 앱을 동작시키면서 Firebase Crashlytics에서 발견할 수 있는 오류와 똑같이 재현할 수 있었습니다.

 

해결 과정

결국에는 빠르게 전환될 가능성이 있는 HomeScreen에 존재하는 ActivityResultLauncher를 안정적으로 등록시킬 수 있도록 수정해야 했습니다.

 

TimerScreen으로 InAppUpdate 로직을 옮기거나, TimerScreen 내부에 LoadingScreen을 추가하여 빠른 전환 시 발생할 수 있는 오류를 막는 방법을 처음에 생각했지만, 사용자가 앱에 접속하자마자 빠르게 SettingScreen으로 이동했을 때 같은 오류가 발생할 수 있는 가능성을 배제할 수 없었습니다.

 

가장 안정적인 방법은 가장 탑 레벨에 있는 MainScreen에 InAppUpdate 로직을 옮겨 안정적으로 ActivityResultLauncher를 유지하는 방법으로 수정하였습니다.

 

안정성은 올라가면서, 앱의 어느 화면에서도 업데이트에 대한 알림을 받고, 유연한 업데이트의 경우 스낵바를 통해 어떤 화면에서도 업데이트를 설치할 수 있다는 장점이 있었습니다. 오히려 이 방법이 더 유연한 업데이트에 적합한 것 같다는 생각도 들었습니다.

 

추가적으로 유연한 업데이트 거절 주기를 하루로 설정하여 사용자가 거절했을 경우 반복적으로 업데이트 요청하는 것을 막고, Loading 화면도 추가하였습니다.

 

관련 PR은 여기를 참고해 주세요.