앱 내에서든 다른 앱에서든 다른 활동을 시작하는 것은 단방향 작업이 아니어도 됩니다. 활동을 시작하고 다시 결과를 받을 수도 있습니다. 예를 들어, 앱에서 카메라 앱을 시작하고 그 결과로 캡처된 사진을 받을 수 있습니다. 또는 사용자가 연락처를 선택하도록 연락처 앱을 시작한 다음 그 결과로 연락처 세부정보를 수신할 수 있습니다.
기본 startActivityForResult()
및 onActivityResult()
API는 모든 API 수준의 Activity
클래스에서 사용할 수 있지만 AndroidX Activity
및 Fragment
클래스에 도입된 Activity Result API를 사용하는 것이 더 좋습니다.
Activity Result API는 결과를 등록하기 위한 구성요소를 제공합니다. 결과를 생성하는 활동을 시작하고 결과가 나오면 처리합니다. 확인합니다
활동 결과에 콜백 등록
결과를 위한 활동을 시작할 때 메모리 부족으로 프로세스와 활동이 소멸될 수 있습니다. 특히 카메라 사용과 같이 메모리를 많이 사용하는 작업의 경우에는 소멸될 확률이 매우 높습니다.
따라서, Activity Result API는 다른 활동을 실행하는 코드 위치에서 결과 콜백을 분리합니다. 결과 콜백은 프로세스와 활동을 다시 생성할 때 사용할 수 있어야 하므로 다른 활동을 실행하는 로직이 사용자 입력 또는 기타 비즈니스 로직을 기반으로만 발생하더라도 활동이 생성될 때마다 콜백을 무조건 등록해야 합니다.
ComponentActivity
또는 Fragment
에 있을 때, Activity Result API에서 제공하는 registerForActivityResult()
API를 통해 결과 콜백을 등록할 수 있습니다. registerForActivityResult()
는 ActivityResultContract
및 ActivityResultCallback
을 가져와서 다른 활동을 실행하는 데 사용할 ActivityResultLauncher
를 반환합니다.
ActivityResultContract
는 결과를 생성하는 데 필요한 입력 유형과 결과의 출력 유형을 정의합니다. 이 API는 사진 촬영, 권한 요청 등과 같은 기본 인텐트 작업의 기본 계약을 제공합니다. 맞춤 계약을 작성할 수도 있습니다.
ActivityResultCallback
은 ActivityResultContract
에 정의된 출력 유형의 객체를 가져오는 onActivityResult()
메서드가 포함된 단일 메서드 인터페이스입니다.
Kotlin
val getContent = registerForActivityResult(GetContent()) { uri: Uri? -> // Handle the returned Uri }
Java
// GetContent creates an ActivityResultLauncher<String> to let you pass // in the mime type you want to let the user select ActivityResultLauncher<String> mGetContent = registerForActivityResult(new GetContent(), new ActivityResultCallback<Uri>() { @Override public void onActivityResult(Uri uri) { // Handle the returned Uri } });
여러 활동 결과 호출이 있고 다른 계약을 사용하거나 별개의 콜백을 원하면 registerForActivityResult()
를 여러 번 호출하여 여러 개의 ActivityResultLauncher
인스턴스를 등록할 수 있습니다. 진행 중인 결과가 올바른 콜백에 전달되도록 프래그먼트나 활동을 만들 때마다 registerForActivityResult()
를 동일한 순서로 호출해야 합니다.
registerForActivityResult()
는 프래그먼트나 활동을 만들기 전에 호출하는 것이 안전하므로 반환되는 ActivityResultLauncher
인스턴스의 멤버 변수를 선언할 때 직접 사용할 수 있습니다.
결과를 위한 활동 실행
registerForActivityResult()
는 콜백을 등록하지만, 다른 활동을 실행하거나 결과 요청을 시작하지 않습니다. 대신, 이 작업은 반환된 ActivityResultLauncher
인스턴스가 담당합니다.
입력이 있으면 런처는 ActivityResultContract
유형과 일치하는 입력을 가져옵니다. launch()
를 호출하면 결과를 생성하는 프로세스가 시작됩니다. 사용자가 후속 활동을 완료하고 반환하면 ActivityResultCallback
의 onActivityResult()
가 다음 예와 같이 실행됩니다.
Kotlin
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 want to let the user select // as the input getContent.launch("image/*") } }
Java
ActivityResultLauncher<String> mGetContent = registerForActivityResult(new GetContent(), new ActivityResultCallback<Uri>() { @Override public void onActivityResult(Uri uri) { // Handle the returned Uri } }); @Override public void onCreate(@Nullable Bundle savedInstanceState) { // ... Button selectButton = findViewById(R.id.select_button); selectButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { // Pass in the mime type you want to let the user select // as the input mGetContent.launch("image/*"); } }); }
오버로드된 launch()
버전을 사용하면 입력 외에도 ActivityOptionsCompat
를 전달할 수 있습니다.
별도의 클래스에서 활동 결과 수신
ComponentActivity
및 Fragment
클래스에서 ActivityResultCaller
인터페이스를 구현하여 registerForActivityResult()
API를 사용할 수 있지만, ActivityResultRegistry
를 직접 사용하여 ActivityResultCaller
를 구현하지 않는 별도의 클래스에서 활동 결과를 수신할 수도 있습니다.
예를 들어, 런처 실행과 함께 계약 등록을 처리하는 LifecycleObserver
를 구현하는 것이 좋습니다.
Kotlin
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() } } }
Java
class MyLifecycleObserver implements DefaultLifecycleObserver { private final ActivityResultRegistry mRegistry; private ActivityResultLauncher<String> mGetContent; MyLifecycleObserver(@NonNull ActivityResultRegistry registry) { mRegistry = registry; } public void onCreate(@NonNull LifecycleOwner owner) { // ... mGetContent = mRegistry.register(“key”, owner, new GetContent(), new ActivityResultCallback<Uri>() { @Override public void onActivityResult(Uri uri) { // Handle the returned Uri } }); } public void selectImage() { // Open the activity to select an image mGetContent.launch("image/*"); } } class MyFragment extends Fragment { private MyLifecycleObserver mObserver; @Override void onCreate(Bundle savedInstanceState) { // ... mObserver = new MyLifecycleObserver(requireActivity().getActivityResultRegistry()); getLifecycle().addObserver(mObserver); } @Override void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { Button selectButton = findViewById(R.id.select_button); selectButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { mObserver.selectImage(); } }); } }
ActivityResultRegistry
API를 사용할 때는 LifecycleOwner
를 가져오는 API를 사용하는 것이 좋습니다. Lifecycle
이 소멸될 때 LifecycleOwner
가 등록된 런처를 자동으로 삭제하기 때문입니다. 그러나 LifecycleOwner
를 사용할 수 없는 경우 대안으로 각 ActivityResultLauncher
클래스를 사용하면 수동으로 unregister()
를 호출할 수 있습니다.
테스트
기본적으로 registerForActivityResult()
는 활동에서 제공하는 ActivityResultRegistry
를 자동으로 사용합니다. 또한 실제로 다른 활동을 실행하지 않고 활동 결과 호출을 테스트하는 데 사용할 수 있는 자체 ActivityResultRegistry
인스턴스를 전달할 수 있는 오버로드를 제공합니다.
앱 프래그먼트를 테스트할 때 개발자는 FragmentFactory
를 사용하여 프래그먼트의 생성자에 ActivityResultRegistry
를 전달하는 테스트 ActivityResultRegistry
를 제공합니다.
예를 들어, TakePicturePreview
계약을 사용하여 미리보기 이미지를 가져오는 프래그먼트는 아래와 비슷하게 작성할 수 있습니다.
Kotlin
class MyFragment( private val registry: ActivityResultRegistry ) : Fragment() { val thumbnailLiveData = MutableLiveData<Bitmap?> val takePicture = registerForActivityResult(TakePicturePreview(), registry) { bitmap: Bitmap? -> thumbnailLiveData.setValue(bitmap) } // ... }
Java
public class MyFragment extends Fragment { private final ActivityResultRegistry mRegistry; private final MutableLiveData<Bitmap> mThumbnailLiveData = new MutableLiveData(); private final ActivityResultLauncher<Void> mTakePicture = registerForActivityResult(new TakePicturePreview(), mRegistry, new ActivityResultCallback<Bitmap>() { @Override public void onActivityResult(Bitmap thumbnail) { mThumbnailLiveData.setValue(thumbnail); } }); public MyFragment(@NonNull ActivityResultRegistry registry) { super(); mRegistry = registry; } @VisibleForTesting @NonNull ActivityResultLauncher<Void> getTakePicture() { return mTakePicture; } @VisibleForTesting @NonNull LiveData<Bitmap> getThumbnailLiveData() { return mThumbnailLiveData; } // ... }
테스트별 ActivityResultRegistry
를 만들 때는 onLaunch()
메서드를 구현해야 합니다. startActivityForResult()
를 호출하지 않고 테스트 구현에서는 dispatchResult()
를 직접 호출하여 테스트에 사용하려는 정확한 결과를 제공할 수 있습니다.
val testRegistry = object : ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
dispatchResult(requestCode, expectedResult)
}
}
전체 테스트는 예상 결과를 만들고, 테스트 ActivityResultRegistry
를 구성하여 프래그먼트에 전달하고, 런처를 직접 또는 Espresso와 같은 다른 테스트 API를 사용하여 트리거한 후 결과를 확인합니다.
@Test
fun activityResultTest {
// Create an expected result Bitmap
val expectedResult = Bitmap.createBitmap(1, 1, Bitmap.Config.RGBA_F16)
// Create the test ActivityResultRegistry
val testRegistry = object : ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
dispatchResult(requestCode, expectedResult)
}
}
// Use the launchFragmentInContainer method that takes a
// lambda to construct the Fragment with the testRegistry
with(launchFragmentInContainer { MyFragment(testRegistry) }) {
onFragment { fragment ->
// Trigger the ActivityResultLauncher
fragment.takePicture()
// Verify the result is set
assertThat(fragment.thumbnailLiveData.value)
.isSameInstanceAs(expectedResult)
}
}
}
맞춤 계약 만들기
ActivityResultContracts
에는 미리 빌드된 ActivityResultContract
클래스가 여러 개 포함되어 사용할 수 있지만, 개발자가 정확히 필요한 유형의 안전한 API를 제공하는 자체 계약을 제공할 수 있습니다.
각 ActivityResultContract
에는 정의된 입력 및 출력 클래스가 필요하므로 입력이 필요하지 않은 경우 Void
를 입력 유형으로 사용합니다(Kotlin에서는 Void?
또는 Unit
사용).
각 계약은 createIntent()
메서드를 구현해야 합니다. 이 메서드는 Context
와 입력을 가져와 startActivityForResult()
와 함께 사용할 Intent
를 구성합니다.
각 계약은 주어진 resultCode
(예: Activity.RESULT_OK
또는 Activity.RESULT_CANCELED
)와 Intent
에서 출력을 생성하는 parseResult()
도 구현해야 합니다.
createIntent()
를 호출하고 다른 활동을 시작하며 parseResult()
를 사용하여 결과를 빌드할 필요 없이 주어진 입력의 결과를 확인할 수 있는 경우 계약은 getSynchronousResult()
를 구현할지 선택할 수 있습니다.
다음 예는 ActivityResultContract
를 구성하는 방법을 보여줍니다.
Kotlin
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) } }
Java
public class PickRingtone extends ActivityResultContract<Integer, Uri> { @NonNull @Override public Intent createIntent(@NonNull Context context, @NonNull Integer ringtoneType) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType.intValue()); return intent; } @Override public Uri parseResult(int resultCode, @Nullable Intent result) { if (resultCode != Activity.RESULT_OK || result == null) { return null; } return result.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); } }
맞춤 계약이 필요하지 않다면 StartActivityForResult
계약을 사용하면 됩니다. 이 계약은 일반 계약으로, Intent
를 입력으로 가져와서 ActivityResult
를 반환하므로 다음 예와 같이 resultCode
와 Intent
를 콜백의 일부로 추출할 수 있습니다.
Kotlin
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)) } }
Java
ActivityResultLauncher<Intent> mStartForResult = registerForActivityResult(new StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { if (result.getResultCode() == Activity.RESULT_OK) { Intent intent = result.getData(); // Handle the Intent } } }); @Override public void onCreate(@Nullable savedInstanceState: Bundle) { // ... Button startButton = findViewById(R.id.start_button); startButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { // The launcher with the Intent you want to start mStartForResult.launch(new Intent(this, ResultProducingActivity.class)); } }); }