2020년 4월 1일, AndroidX Activity-1.2.0-alpha03 / Fragment-1.3.0-alpha03 버전으로 업데이트되었습니다. ActivityResult가 등장 후 두 번째 업데이트입니다.
첫 번째 등장에 관한 내용은 다음 글을 참고하세요. New ActivityResultRegistry
public abstract class ActivityResultContract<I, O> {
- public abstract @NonNull Intent createIntent(@SuppressLint("UnknownNullness") I input);
+ public abstract @NonNull Intent createIntent(@NonNull Context context,
+ @SuppressLint("UnknownNullness") I input);
}
기존 Intent 생성 시에는 I 타입의 Input만 전달되었지만, Contenxt가 새롭게 추가되었습니다. Context가 필요성의 가장 쉬운 예로는 Activity 이동에 Context를 사용할 수 있습니다.
Intent(Context, SecondActivity::class.java)
기존 Activity-1.2.0-alpha02 / Fragment-1.3.0-alpha02에서는 중요한 버그가 하나 있었습니다. Permission 요청에 희한 수락한 후 재요청 시 응답이 반환되지 않는 버그가 발생했습니다. 다행히 두 번째 업데이트인 Activity-1.2.0-alpha03 / Fragment-1.3.0-alpha03에는 정상적으로 수정되었습니다. 어떻게 수정을 했는지 체크해보겠습니다.
Android는 현재 위치에 접근하기 위해서는 권한 취득이 필요합니다. 실제 위치 취득 전, ActivityResultContract를 사용해서 권한 요청/응답을 처리를 할 수 있습니다.
val requestLocation = prepareCall(RequestPermission(), ACCESS_FINE_LOCATION) { isGranted ->
toast("Location granted: $isGranted")
}
ComponentActivity에서 구현하고 있는 ActivityResultRegistry의 내부가 변경되었습니다. 아래의 코드가 실제 변경이 발생한 부분만 가져왔습니다.
public class ComponentActivity ... {
...
private ActivityResultRegistry mActivityResultRegistry = new ActivityResultRegistry() {
@Override
public <I, O> void invoke(...) {
// ▼▼▼▼▼ Add Code ▼▼▼▼▼
// Immediate result path
final ActivityResultContract.SynchronousResult<O> synchronousResult =
contract.getSynchronousResult(activity, input);
if (synchronousResult != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
dispatchResult(requestCode, synchronousResult.getValue());
}
});
return;
}
// ▲▲▲▲▲ Add Code ▲▲▲▲▲
// requestPermissions / startActivityForResult 호출
...
}
};
...
}
기존 버전에서는 Intent#getAction() 의 결과에 따라 requestPermissions() / startActivityForResult()을 호출했습니다. 해당 작업을 실행하기 전, 처리 가능 여부를 먼저 처리하는 작업이 추가되었습니다. 그 후 ActivityResultContract.SynchronousResult(context, input) 가 null인 경우 ActivityResultRegistry#dispatchResult(int, int, Intent) 를 호출하여 결과를 전달합니다. null 인 경우에는 기존과 동일하게 requestPermissions() / startActivityForResult() 을 처리합니다.
현재 AndroidX에서 ActivityResultContract.SynchronousResult 를 NonNull로 반환하는 케이스는 Permission을 체크하는 경우입니다.
ActivityResultContracts.RequestPermission 의 경우, 기존 버전에는 없었던 코드로 사전 체크 방식을 확인할 수 있습니다. 반환 타입은 지금에는 무시해도 괜찮습니다. 코드에서 확인 가능한 부분은 input으로 넘겨온 퍼미션이 유효한지 체크
하는 코드입니다.
public final class ActivityResultContracts {
...
public static final class RequestPermission extends ActivityResultContract<String, Boolean> {
@Override
public @Nullable SynchronousResult<Boolean> getSynchronousResult(
@NonNull Context context, @Nullable String input) {
if (input == null) {
return new SynchronousResult<>(false);
} else if (ContextCompat.checkSelfPermission(context, input)
== PackageManager.PERMISSION_GRANTED) {
return new SynchronousResult<>(true);
} else {
return null;
}
}
}
...
}
AndroidX 내부 코드를 사용하지 않고서 직접 ActivityResultContract 를 커스텀하는 경우 권한 혹은 Intent 요청 전 사전에 처리가 가능한 경우
에는 위 함수를 제대로 구현할 필요가 있습니다.
ActivityResultContract 내부에 추가된 코드들은 매우 간단합니다. 앞서 설명한 대로 getSynchronousResult(context, input) 함수를 통해서 사전 처리하는 함수입니다.
public abstract class ActivityResultContract<I, O> {
public @Nullable SynchronousResult<O> getSynchronousResult(
@NonNull Context context,
@SuppressLint("UnknownNullness") I input) {
return null;
}
public static final class SynchronousResult<T> {
private final @SuppressLint("UnknownNullness") T mValue;
public SynchronousResult(@SuppressLint("UnknownNullness") T value) {
this.mValue = value;
}
public @SuppressLint("UnknownNullness") T getValue() {
return mValue;
}
}
}
또한 getSynchronousResult(context, input) 의 결과로 ActivityResultContract.SynchronousResult 타입을 전달합니다. 이 타입은 단순 결과 타입을 랩핑한 Class입니다.
아주 작은 변화이지만 실제로 내부는 어떻게 변경이 되었는지 파악을 해봤습니다. 다음으로는 Common 형태로 제공되는 ActivityResultContracts 의 변경을 살펴보겠습니다.
Intent.ACTION_DIAL
으로 전화 요청을 할 수 있지만, 해당 Intent Action은 Result를 반환하지 않으므로 제거되었습니다.new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, input);
new Intent(MediaStore.ACTION_VIDEO_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, input);
new Intent(Intent.ACTION_PICK)
.setType(ContactsContract.Contacts.CONTENT_TYPE);
Dial
과 동일하게 Result 미반환이라는 이유로 빛을 보지 못하고 사라진 타입입니다.new Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(input);
new Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(input)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
new Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra(Intent.EXTRA_MIME_TYPES, input)
.setType("*/*")
new Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra(Intent.EXTRA_MIME_TYPES, input)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
.setType("*/*");
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& input != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input);
}
new Intent(Intent.ACTION_CREATE_DOCUMENT)
.setType("*/*")
.putExtra(Intent.EXTRA_TITLE, input);
느리지만 Activity/Fragment에 조금씩 변화가 발생하고 있습니다. 저로서는 흥미로운 일입니다.
그리고, 이번 업데이트에는 본 글에서 다루지 않은 다른 변화들도 존재합니다. 한 번쯤 릴리즈 노트를 챙겨보면서 변화의 흐름을 보는 재미를 느꼈으면 합니다.
comments powered by Disqus
Subscribe to this blog via RSS.