15
9

More than 3 years have passed since last update.

ActivityResultContractsの仕組みを調べてみる

Last updated at Posted at 2020-07-05

activity:1.2.0、fragment:1.3.0 からActivityやFragmentの startActivityForResult / onActivityResultrequestPermissions / onRequestPermissionsResult あたりがdeprecatedになるそうですね。ActivityResultContractsを使えと言うことだそうです。
ちょっと使ってみたところ、良さそうなんだけど、どういう仕組みになっているのか気になったので調べてみました。

おさらい

startActivityForResult いままで

今まで、他のActivityから結果を受けとるには以下のようにしていました。

リクエスト

startActivityForResult(intent, REQUEST_CODE)

結果の受け取り

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            Toast.makeText(this, "result: $data", Toast.LENGTH_LONG).show()
        }
        return
    }
    super.onActivityResult(requestCode, resultCode, data)
}

ちなみに、呼び出し先ではsetResultでresultCodeと結果を詰めたIntentを設定してActivityを終了させることで呼び出し元に結果を返します。

setResult(Activity.RESULT_OK, intent)
finish()

startActivityForResult これから

registerForActivityResultを使ってコールバックを登録し、ActivityResultLauncherのインスタンスを取得しておきます。

private val launcher = registerForActivityResult(StartActivityForResult()) {
    Toast.makeText(this,"result: $it", Toast.LENGTH_LONG).show()
}

リクエストを投げるときはlaunchメソッドを呼び出します。

launcher.launch(intent)

返ってくると registerForActivityResult に渡したコールバックがコールされます。
コールバックの引数は ActivityResultonActivityResult の 第二引数の resultCode: Int と 第三引数 data: Intent を格納したクラスです。

requestPermission これまで

requestPermissionはstartActivityForResultに似た使い方をしますが、リクエストメソッドも、結果を受けとるメソッドも異なります。

リクエスト

ActivityCompat.requestPermissions(this, arrayOf(WRITE_EXTERNAL_STORAGE), REQUEST_CODE)

結果の受け取り

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    if (requestCode == REQUEST_CODE) {
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "result: granted", Toast.LENGTH_LONG).show()
        }
        return
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

requestPermission これから

registerForActivityResult の第一引数に RequestPermission を指定してコールバックを登録

private val launcher = registerForActivityResult(RequestPermission()) {
    if (it) {
        Toast.makeText(this, "result: granted", Toast.LENGTH_LONG).show()
    }
}

lauchメソッドでリクエスト、今度はStringが引数になっていて、リクエストするPermissionを渡します。

launcher.launch(WRITE_EXTERNAL_STORAGE)

コールバックの引数はbooleanになっていて、成功した場合にtrueが返るようになっています。

また、複数のパーミッションを同時にリクエストする場合は RequestMultiplePermissions を使います。
こちらの場合はlaunchの引数が Array<String> になり、コールバックの引数は Map<String, Boolean> になります。

ActivityResultContractsの仕組み

registerForActivityResult の仕組み

ActivityResultRegistry を引数に追加しています。

ComponentActivity.java
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull ActivityResultContract<I, O> contract,
        @NonNull ActivityResultCallback<O> callback) {
    return registerForActivityResult(contract, mActivityResultRegistry, callback);
}

ActivityResultRegistry のregisterをコールしていますね。第一引数は key だそうですが、mNextLocalRequestCode は AtomicIntegerでコールされる度にインクリメントされるようです。

ComponentActivity.java
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull final ActivityResultContract<I, O> contract,
        @NonNull final ActivityResultRegistry registry,
        @NonNull final ActivityResultCallback<O> callback) {
    return registry.register(
            "activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}

registerはちょっと長いメソッドです。

ActivityResultRegistry.java
@NonNull
public final <I, O> ActivityResultLauncher<I> register(
        @NonNull final String key,
        @NonNull final LifecycleOwner lifecycleOwner,
        @NonNull final ActivityResultContract<I, O> contract,
        @NonNull final ActivityResultCallback<O> callback) {

    final int requestCode = registerKey(key);
    mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));

    Lifecycle lifecycle = lifecycleOwner.getLifecycle();

    final ActivityResult pendingResult = mPendingResults.getParcelable(key);
    if (pendingResult != null) {
       // 略
    }

    lifecycle.addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
                @NonNull Lifecycle.Event event) {
            if (Lifecycle.Event.ON_DESTROY.equals(event)) {
                unregister(key);
            }
        }
    });

    return new ActivityResultLauncher<I>() {
        @Override
        public void launch(I input, @Nullable ActivityOptionsCompat options) {
            onLaunch(requestCode, contract, input, options);
        }

        @Override
        public void unregister() {
            ActivityResultRegistry.this.unregister(key);
        }

        @NonNull
        @Override
        public ActivityResultContract<I, ?> getContract() {
            return contract;
        }
    };
}

最初にkeyからrequestCodeを作っています。
registerKeyではすでに登録済みならそのrequestCodeを、登録されていなければ 0x0000ffff を初期値として順次インクリメントした値が割り振られるようです。

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

その後の returnまでの処理は、Activityが再生成されて、コールバックが再登録されるまでの間に受け取った結果が合った場合に、コールバックを呼び出す処理ですね。
それと、onDestroyで自動的にunregisterしているので、unregisterを呼び出す必要も無いし、Fragmentから登録した場合も、メモリリークなどが起こらないようになっています。
結局戻り値は ActivityResultLauncher になっていて、launch がコールされたら onLaunch が呼び出されます。

onLaunchはabstructメソッドで、実装自体はこちらになります。さすがに長いしのでリンクにさせてもらいます。

かなり泥臭いことやってますね。requestPermissionとstartActivityForResult って別の処理なのにどうやって共通化したのかと思いきやこういう仕組みでした。
getSynchronousResult のところでは、requestPermissionですべてのパーミッションがとれている場合のように、リクエストする必要が無い場合は、即座にコールバックだけコールする仕組みも用意されていますね。

結果の受け取りのところは以下のようになっていて、onActivityResultonRequestPermissionsResultdispatchResultをコールしています。

ComponentActivity.java
@CallSuper
@Override
@Deprecated
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    if (!mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

@CallSuper
@Override
@Deprecated
public void onRequestPermissionsResult(
        int requestCode,
        @NonNull String[] permissions,
        @NonNull int[] grantResults) {
    if (!mActivityResultRegistry.dispatchResult(requestCode, Activity.RESULT_OK, new Intent()
            .putExtra(EXTRA_PERMISSIONS, permissions)
            .putExtra(EXTRA_PERMISSION_GRANT_RESULTS, grantResults))) {
        if (Build.VERSION.SDK_INT >= 23) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

最終的に以下のようにコールバックされています。

ActivityResultRegistry.java
@MainThread
public final boolean dispatchResult(int requestCode, int resultCode, @Nullable Intent data) {
    String key = mRcToKey.get(requestCode);
    if (key == null) {
        return false;
    }
    doDispatch(key, resultCode, data, mKeyToCallback.get(key));
    return true;
}

private <O> void doDispatch(String key, int resultCode, @Nullable Intent data,
        @Nullable CallbackAndContract<O> callbackAndContract) {
    if (callbackAndContract != null && callbackAndContract.mCallback != null) {
        ActivityResultCallback<O> callback = callbackAndContract.mCallback;
        ActivityResultContract<?, O> contract = callbackAndContract.mContract;
        callback.onActivityResult(contract.parseResult(resultCode, data));
    } else {
        mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
    }
}

やっていることはそんな複雑なことではないですね。

調べた結果

分かったこと

ちょっと使ってみて最初に疑問だったのが肝心のActivityResultContracts の扱い
registerForActivityResult(StartActivityForResult()) { } って、何に使われてるんだ?って疑問がありましたが、中を読めば分かりました。
launchの引数と、コールバックの引数はジェネリックスになっていて、これに対する型情報を提供するとともに、launchの引数からIntentを、また結果のresultCodeとdataからコールバックの引数を作る役割を持っています。
dataから読み出した結果を返すようにした、独自のActivityResultContractsを作ることもできますね。

よく分からなかったこと

気になる点があります、以下の記事で書かれているとおり、requestCodeがregisterの順序に依存してしまっています。
https://medium.com/@star_zero/a5a408c15f50
Activityが復元された場合にも結果を受けとるには、リクエスト時と同じ順序でregisterForActivityResultを登録しておかないといけません。
Activityで完結している場合はコンストラクタやonCreateでの登録をすれば問題無いのですが、Fragmentの場合、ActivityのActivityResultRegistoryに、FragmentのonCreate以降に登録されることになります。
特に動的に配置するFragmentの場合、順序の制御ができないのでどうすればいいのかがよく分かりませんでした。
改めて調べたところ、Fragmentごとにユニークなプレフィックス付きで、Fragmentごとの順序に基づくkeyが作られていました。ですのでFragment内での順序が固定されていればActivityから見た順序が違っていても大丈夫でした。

ちょととモヤモヤしてること

Activity/Fragmentのメソッドで結果を受け取っていて、リクエストと結果の処理が分離してしまったりであまり綺麗に書けなかった処理が、個別のコールバックで書けるようになるのでいい感じです。
しかし、実際のリクエストはlaunchで指定する必要があるので、リクエストとコールバックをまとめるには少し足りていない感があります。
独自の ActivityResultContracts を作成するなどで解決できる問題ではありますが、できればそこも含めてライブラリ側で提供して欲しいなぁという気がします。

と、良さそうなんだけど、もうちょっとなところがある気がしました。
なんだかんだ言っても、現在はAlphaなので、これからどうなるか見守りたいです。

15
9
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
15
9