LoginSignup
27
11

More than 3 years have passed since last update.

ActivityResultContractのコードを少し読んでみる

Last updated at Posted at 2020-03-20

Activity 1.2.0-alpha02 と Fragment 1.3.0-alpha02で追加されたAPIのようで、型安全にstartActivityForResultを扱えるようです。またテストもできるようです。

コード例
AppCompatActivityやFragmentでprepareCall()でlauncherを用意して、launchを呼ぶとstartActivityForResultを行うことができるようです。

MainActivity.kt
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)
}
SecondActivity.kt
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を継承しています。

ActivityResultContracts.java
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で受け取る型も型引数で指定して書いていくことができます。

MainActivity.kt
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()しています。

ComponentActivity.java
    @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()を利用すると以下のように書くことができます。

MainActivity.kt
        // ** ↓ **
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()などになっています。
image.png

今まであったrequest codeはどうなったか?

今まではstartActivityForResultを呼び出すときに呼び出しと返却を識別するために使うrequest codeというのが必要でしたが、それが利用するときに必要なくなりました。こちらもAutoIncrementして管理しています。また、プロセスが死んでも再度使えるようにonSaveInstanceStateでrequest codeとActivityResultRegistoryのkeyのペアを管理して、保存してうまくやってくれるようです

ActivityResultRegistry.java
    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で作られたコールバックを突っ込むため、
これだけ見るとリークしてそうに見えます。

Fragment.java
    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として渡しています。

Fragment.java
registerActivityResultCallback(
                                    key, Fragment.this  // **←ここでLifecycleOwnerとして渡す**

また、以下のようにON_DESTROYでunregisterActivityResultCallbackするため問題なさそうに見えます。

ActivityResultRegistry.java
       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

27
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
11