활동에서 결과 가져오기

앱 내에서든 다른 앱에서든 다른 활동을 시작하는 것은 단방향 작업이 아니어도 됩니다. 활동을 시작하고 다시 결과를 받을 수도 있습니다. 예를 들어, 앱에서 카메라 앱을 시작하고 그 결과로 캡처된 사진을 받을 수 있습니다. 또는 사용자가 연락처를 선택하도록 연락처 앱을 시작한 다음 그 결과로 연락처 세부정보를 수신할 수 있습니다.

기본 startActivityForResult()onActivityResult() API는 모든 API 수준의 Activity 클래스에서 사용할 수 있지만 AndroidX ActivityFragment 클래스에 도입된 Activity Result API를 사용하는 것이 더 좋습니다.

Activity Result API는 시스템에서 결과가 전달되면 이를 등록, 실행, 처리하기 위한 구성요소를 제공합니다.

활동 결과에 콜백 등록

결과를 위한 활동을 시작할 때 메모리 부족으로 프로세스와 활동이 소멸될 수 있습니다. 특히 카메라 사용과 같이 메모리를 많이 사용하는 작업의 경우에는 소멸될 확률이 매우 높습니다.

따라서, Activity Result API는 다른 활동을 실행하는 코드 위치에서 결과 콜백을 분리합니다. 결과 콜백은 프로세스와 활동을 다시 생성할 때 사용할 수 있어야 하므로 다른 활동을 실행하는 로직이 사용자 입력 또는 기타 비즈니스 로직을 기반으로만 발생하더라도 활동이 생성될 때마다 콜백을 무조건 등록해야 합니다.

ComponentActivity 또는 Fragment에 있을 때, Activity Result API에서 제공하는 registerForActivityResult() API를 통해 결과 콜백을 등록할 수 있습니다. registerForActivityResult()ActivityResultContractActivityResultCallback을 가져와서 다른 활동을 실행하는 데 사용할 ActivityResultLauncher를 반환합니다.

ActivityResultContract는 결과를 생성하는 데 필요한 입력 유형과 결과의 출력 유형을 정의합니다. 이 API는 사진 촬영, 권한 요청 등과 같은 기본 인텐트 작업의 기본 계약을 제공합니다. 맞춤 계약을 생성할 수도 있습니다.

ActivityResultCallbackActivityResultContract에 정의된 출력 유형의 객체를 가져오는 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()를 호출하면 결과를 생성하는 프로세스가 시작됩니다. 사용자가 후속 활동을 완료하고 반환하면 ActivityResultCallbackonActivityResult()가 다음 예와 같이 실행됩니다.

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를 전달할 수 있습니다.

별도의 클래스에서 활동 결과 수신

ComponentActivityFragment 클래스에서 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를 반환하므로 다음 예와 같이 resultCodeIntent를 콜백의 일부로 추출할 수 있습니다.

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));
        }
    });
}