Activity 1.2.0-alpha02 と Fragment 1.3.0-alpha02で追加されたAPIのようで、型安全にstartActivityForResultを扱えるようです。またテストもできるようです。
コード例
AppCompatActivityやFragmentでprepareCall()
でlauncherを用意して、launch
を呼ぶとstartActivityForResult
を行うことができるようです。
val intent = Intent(this, SecondActivity::class.java)
val launcher: ActivityResultLauncher<Intent> = prepareCall(
ActivityResultContracts.StartActivityForResult()
) { activityResult: ActivityResult ->
Log.d("MainActivity", activityResult.toString())
// D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
}
fab.setOnClickListener { view ->
launcher.launch(intent)
}
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(Activity.RESULT_OK, Intent().putExtra("my-data", "data"))
finish()
}
}
コードを剥がしていく
ちょっと魔法みたいなので、中身を覗いて、今のコードに適応させてみましょう。
ActivityResultContractを直接使ってみる
ActivityResultContracts.StartActivityForResultクラスは以下のように ActivityResultContract
を継承しています。
public class ActivityResultContracts {
private ActivityResultContracts() {}
...
public static class StartActivityForResult
extends ActivityResultContract<Intent, ActivityResult> {
@NonNull
@Override
public Intent createIntent(@NonNull Intent input) {
return input;
}
@NonNull
@Override
public ActivityResult parseResult(int resultCode, @Nullable Intent intent) {
return new ActivityResult(resultCode, intent);
}
}
そのためActivityResultContract
を使って書き直して以下と同じです。
ここでは型引数としてIntentとActivityResultクラスをとっています。このように自分でstartActivityのときに使いたい引数の型とActivityResultで受け取る型も型引数で指定して書いていくことができます。
val launcher: ActivityResultLauncher<Intent> = prepareCall(
// ** ↓ **
object : ActivityResultContract<Intent, ActivityResult>() {
override fun createIntent(input: Intent): Intent {
return input
}
override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult {
return ActivityResult(resultCode, intent)
}
}
// ** ↑ **
) { activityResult: ActivityResult ->
Log.d("MainActivity", activityResult.toString())
// D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
}
ActivityResultRegistryを直接使ってみる
またComponentActivity.prepareCallは以下のように、mActivityResultRegistryにregisterActivityResultCallback()しています。
@NonNull
@Override
public <I, O> ActivityResultLauncher<I> prepareCall(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return prepareCall(contract, mActivityResultRegistry, callback);
}
@NonNull
@Override
public <I, O> ActivityResultLauncher<I> prepareCall(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return registry.registerActivityResultCallback(
"activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}
@NonNull
public ActivityResultRegistry getActivityResultRegistry() {
return mActivityResultRegistry;
}
そのためComponentActivity.getActivityResultRegistry()
をしてActivityResultRegistry.registerActivityResultCallback()
を利用すると以下のように書くことができます。
// ** ↓ **
val launcher: ActivityResultLauncher<Intent> = activityResultRegistry
.registerActivityResultCallback(
"activity_rq#0", // ←この数字は本来は呼び出すたびにincrementが必要
// ** ↑ **
object : ActivityResultContract<Intent, ActivityResult>() {
override fun createIntent(input: Intent): Intent {
return input
}
override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult {
return ActivityResult(resultCode, intent)
}
}
) { activityResult: ActivityResult ->
Log.d("MainActivity", activityResult.toString())
// D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
}
registerActivityResultCallback(key, ActivityResultCallback, ActivityResultContract)
を使うとActivityResultRegistory
が持っているHashMapにputされます
ComponentActivity周りのかんたんな構造
単純にActivityがActivityResultRegistoryを持っていて、ActivityResultRegistoryがActivityResultContractとActivityResultCallbackを持っているようです。
このHashMapのキーはregisterActivityResultCallback
に渡される "activity_rq#" + mNextLocalRequestCode.getAndIncrement()
などになっています。
今まであったrequest codeはどうなったか?
今まではstartActivityForResultを呼び出すときに呼び出しと返却を識別するために使うrequest codeというのが必要でしたが、それが利用するときに必要なくなりました。こちらもAutoIncrementして管理しています。また、プロセスが死んでも再度使えるようにonSaveInstanceState
でrequest codeとActivityResultRegistoryのkeyのペアを管理して、保存してうまくやってくれるようです
private int registerKey(String key) {
Integer existing = mKeyToRc.get(key);
if (existing != null) {
return existing;
}
int rc = mNextRc.getAndIncrement();
bindRcKey(rc, key);
return rc;
}
Fragmentでメモリリークしないのか?
Fragment.prepareCall()
の実装を見るとFragmentでON_CREATEになったときにgetActivity().getActivityResultRegistry().registerActivityResultCallback
します。Activityのスコープの変数にFragmentで作られたコールバックを突っ込むため、
これだけ見るとリークしてそうに見えます。
public <I, O> ActivityResultLauncher<I> prepareCall(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
...
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
@NonNull Lifecycle.Event event) {
if (Lifecycle.Event.ON_CREATE.equals(event)) {
ref.set(getActivity()
.getActivityResultRegistry()
// **ここでregisterActivityResultCallback**
.registerActivityResultCallback(
key, Fragment.this, contract, callback));
}
}
});
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input) {
...
}
};
}
しかし、以下でLifecycleOwnerとして渡しています。
registerActivityResultCallback(
key, Fragment.this // **←ここでLifecycleOwnerとして渡す**
また、以下のようにON_DESTROYでunregisterActivityResultCallbackするため問題なさそうに見えます。
lifecycle.addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
@NonNull Lifecycle.Event event) {
if (Lifecycle.Event.ON_DESTROY.equals(event)) {
unregisterActivityResultCallback(key);
}
}
});
まとめ
- ActivityResultContractに型引数を渡すことで、渡す方と受け取る型を指定していけそう。
- ComponentActivity.ActivityResultRegistryでコールバックなどは管理されている。
- この部分に関してFragmentのリークは気にしなくても良さそう。
参考
https://android-review.googlesource.com/c/platform/frameworks/support/+/1233518
https://developer.android.com/training/basics/intents/result
https://medium.com/@star_zero/activityresultcontract%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Factivity%E9%96%93%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%97%E3%81%91%E6%B8%A1%E3%81%97-17052bfafde0