앱 내에서든 다른 앱에서든 다른 활동을 시작하는 것은 단방향 작업이 아니어도 됩니다. 활동을 시작하고 다시 결과를 받을 수도 있습니다. 예를 들어, 앱에서 카메라 앱을 시작하고 그 결과로 캡처된 사진을 받을 수 있습니다. 또는 사용자가 연락처를 선택하도록 연락처 앱을 시작한 다음 그 결과로 연락처 세부정보를 수신할 수 있습니다.
기본 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()
메서드가 포함된 단일 메서드 인터페이스입니다.
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
// 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()
가 다음 예와 같이 실행됩니다.
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/*")
}
}
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
를 구현하는 것이 좋습니다.
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()
}
}
}
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
계약을 사용하여 미리보기 이미지를 가져오는 프래그먼트는 아래와 비슷하게 작성할 수 있습니다.
class MyFragment(
private val registry: ActivityResultRegistry
) : Fragment() {
val thumbnailLiveData = MutableLiveData<Bitmap?>
val takePicture = registerForActivityResult(TakePicturePreview(), registry) {
bitmap: Bitmap? -> thumbnailLiveData.setValue(bitmap)
}
// ...
}
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
를 구성하는 방법을 보여줍니다.
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)
}
}
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
를 콜백의 일부로 추출할 수 있습니다.
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))
}
}
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));
}
});
}