외부 Activity를 실행할 때, 이전 방식은 startActivityForResult()를 이용하여 Intent를 날리고 onActivityResult() callback으로 결과를 받아오는 방식이었다. 새롭게 권장하는 방식은 AndroidX 라이브러리의 Activity/Fragment 를 이용하여 RegisterForActivityResult()를 사용하는 방식이다.
정확한 장점을 설명하기에 아직 내 이해도가 부족하지만, 기존 방식이 좀 무식하긴 했다. 인텐트로 외부 액티비티를 실행하고, 뭘 실행했든지 onActivityResult() 하나의 콜백에서 request code로 분류하며 전부 처리해야 했다. 또한, 인텐트 설정, 프래그먼트 내에서 호출시 잘못된 사용등으로 onActivityResult()가 제대로 호출되지 않는 경우도 많았다. 변경된 방식의 장점이 많이 있지만 그중 하나를 꼽자면, 복잡한 permission 요청과정이 단순해진다. 주저할 이유가 없다. 기존 방식을 빨리 잊고 새로운 방식을 알아보도록 하자.
새로운 API는 registerForActivityResult()이다. 이는 androidx.activity나 androidx.fragment에서 제공되는데, 다음과 같은 의존성 라이브러리를 추가해도 되지만, 아마도 androidx.appcompat 에 포함되었는지 추가하지 않아도 사용가능하다.
// activity, fragment for permissions
implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.3.6"
registerForActivityResult()는 두개의 인자를 입력받는다. 하나는 ActivityResultContract이고, 나머지 하나는 ActivityResultCallback이다. Contract는 실행할 activity정보, 입출력 값에 대한 정보를 갖게된다. 사용자가 custom으로 정의할 수도 있으나, 기본 값들을 제공하고 있으니 이를 사용하면 간단하게 사용가능하다. Callback은 Contract의 출력값에 대해 실행되는 callback 함수가 들어간다. callback은 람다함수 형태를 사용하면 코드가 다음과 같이 단순해진다.
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
Contract로 GetContent()를 사용하고 있는데, 이는 content://Uri 형식을 받는 기본 제공 Contract이다.
registerForActivityResult()는 호출시 Contract와 Callback을 등록만하고 바로 실행되지 않는다. 대신 ActivityResultLauncher를 리턴하는데, 이 리턴받은 Launcher의 launch()를 호출해서 실행하게 된다. 예제를 살펴보면 다음과 같다.
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
val selectButton = findViewById<Button>(R.id.select_button)
selectButton.setOnClickListener {
// Pass in the mime type you'd like to allow the user to select
// as the input
getContent.launch("image/*")
}
}
새로운 방식이 기존 방식과 완전히 차별화 될 수 있는 이유로, Activity나 Fragment가아닌 별도의 클래스에서 activity result를 받을 수 있다. 그 방법은 ActivityResultRegistry를 직접 이용하는 것이다. 예제 코드는 다음과 같다.
class MyLifecycleObserver(private val registry : ActivityResultRegistry)
: DefaultLifecycleObserver {
lateinit var getContent : ActivityResultLauncher<String>
override fun onCreate(owner: LifecycleOwner) {
getContent = registry.register("key", owner, GetContent()) { uri ->
// Handle the returned Uri
}
}
fun selectImage() {
getContent.launch("image/*")
}
}
class MyFragment : Fragment() {
lateinit var observer : MyLifecycleObserver
override fun onCreate(savedInstanceState: Bundle?) {
// ...
observer = MyLifecycleObserver(requireActivity().activityResultRegistry)
lifecycle.addObserver(observer)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val selectButton = view.findViewById<Button>(R.id.select_button)
selectButton.setOnClickListener {
// Open the activity to select an image
observer.selectImage()
}
}
}
라이프 사이클 옵저버를 만들고 여기에서 registerForActivityResult()대신에 registry.register()를 이용하여 직접 등록을 하고 있다. registry는 contract와 callback이 등록되는 별도의 공간이라고 생각하면 된다. LifecycleOwner를 이용하여 예제가 나온 이유는 Lifecycle에 따라 알아서 등록된 launcher들을 제거하기 위함이다. 만약 수동으로 제거하는 경우에는 ActivityResultLauncher.unregister()를 이용할 수 있다고 한다.
위와 같은 방법을 사용하여 구현하면, Activity나 Fragment로부터 해당 구현을 완전히 분리해 낼수가 있기 때문에 확실한 장점이라고 생각할 수 있다. 또한, 유닛 테스트에도 기본으로 사용하는 Registry대신 test registry를 직접구현하여 넣어줄 수 있어 테스트에 용이하다.
마지막으로 default contract가 아닌 custom contract의 사용을 알아보자. 예제는 다음과 같다.
class PickRingtone : ActivityResultContract<Int, Uri?>() {
override fun createIntent(context: Context, ringtoneType: Int) =
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
}
override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
if (resultCode != Activity.RESULT_OK) {
return null
}
return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
}
custom contract를 만들려면 입력과 출력값을 지정해줘야한다. 위 예제에서 ActivityResultContract를 상속받으며 generic 타입에 <Int, Uri?>를 지정해주고 있는걸 볼 수 있다.
또한 바디에서는 createIntent()와 parseResult()를 오버라이드 해서 구현해야 한다. createIntent()는 말 그대로 사용할 인텐트를 만들어 리턴해준다. 예전방식에서 startActivityForResult()에 Intent를 넘겨줬었는데, 그걸 생각하면 된다. parseResult()는 resultCode와 Intent형태의 result가 리턴되는걸 알 수 있다. result code값으로 성공 여부를 판단하고, 넘어온 Intent로부터 필요한 값을 추출하고 있는걸 확인 할 수 있다. 이 과정을 보면, startActivityForResult()를 사용하던 방식이 없어진게 아니라, 최대한 추상화 시켜놓고, 기본적인 것들은 default contract로 미리 구현해놨음을 이해할 것이다. 위 예제에서 보이듯이 custom contract는 Intent를 사용하는 예전 방식에 익숙하다면 손쉽게 구현할 수 있는 내용이다.
이렇게 custom contract를 구현하는게 정석이고 깔끔하겠지만, 주고받는 값이 복잡하지 않다면, StartActivityForResult contract 를 사용할 수도 있다. 예제는 다음과 같다.
val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
// Handle the Intent
}
}
override fun onCreate(savedInstanceState: Bundle) {
// ...
val startButton = findViewById(R.id.start_button)
startButton.setOnClickListener {
// Use the Kotlin extension in activity-ktx
// passing it the Intent you want to start
startForResult.launch(Intent(this, ResultProducingActivity::class.java))
}
}
custom contract를 정의하지 않는 대신에, launcher에 직접 Intent를 넘겨주고, callback에서 resultCode확인과 Intent를 직접받는 방식인데, 예전방식과 가장 가까운 방식으로 보일 것이다. 리턴값이 필요없거나 간단한 경우에 가장 적합해 보인다.
지금까지 살펴본 것과 같이, 안드로이드 초창기부터 사용되던 StartActivityResult()로 외부 Activity를 실행하던 방식은 deprecated되고 registerForActivityResult()로 대체되었다. 충분히 납들할만한 변화이고 작성할 코드도 더 깔끔해지기 때문에 만족할 것이다. 기본적으로 Activity간 인터렉션이 Intent를 이용한다는 사실엔 변함이 없다. 익숙하다고 예전방식에 집착하기보다 좋은거 빨리 학습해서 갈아타자.
안드로이드 공식 문서 : https://developer.android.com/training/basics/intents/result
아마도 새 방식이 좋은 이유? : https://www.mongodb.com/developer/article/realm-startactivityforresult-registerForActivityResult-deprecated-android-kotlin/
1 thought on “Android: onActivityResult()의 대안 RegisterForActivityResult()”