現在は startActivityForResult
/ onActivityResult
の代わりに Activity Result API
というAndroidXライブラリが用意されており、これら従来のAPIは androidx.activity
の最新alpha版で既にdeprecatedになっています。
そのため通常のアプリにおいては、本記事で取り扱うAPIは利用すべきではありません。恐らく、レガシーな開発環境を考慮に入れたライブラリ開発等でのみ必要なナレッジになるかと思います。
Android開発において画面間での結果受け渡しを行う方法は複数思い当たるが、最もオーソドックスかつあらゆるライフサイクル都合に対応可能なのが startActivityForResult
/ onActivityResult
を用いた方法になる。
このAPIはActivityだけでなくFragment側にも存在し、Fragmentから他Activityを呼び出し、その結果を呼び出し元のFragmentで受け取る、なんていうことも可能だ。
ご存知のとおりAndroidは歴史的理由で "Fragment" と呼ばれる実装が2通り存在し、これは継承関係を持っていない。ここでは便宜上、
としていく。
検証用アプリを用意した。
ざっくり、下記のような実装になっている:
startActivityForResult
を叩き、次のActivityを表示するこれを実行すると、PlatformLauncher#onActivityResult
に結果が返る。呼び出し元はFragmentなので、この親Fragmentには特段通知が行かない。直感に反しない純粋な挙動だ。
上記検証用アプリ内のもうひとつのボタンは、このFragmentをAndroidXのFragment実装(SupportLauncherとする)に変更している。
この状態で実行すると、当然期待どおり SupportLauncher#onActivityResult
に結果が返るのだが、さらにこの親のActivityの onActivityResult
にも結果が返るのである。
直感に反するどころか、そもそもFramework APIと異なる挙動になる。それに、親Activity側には自分の指定した値と全く異なるrequestCodeが返っている。
これは何か。
Framework側のresult実装は(あくまでイメージなので厳格性に欠けるが)ざっくり下記のような仕様になっている:
startActivityForResult
が実行されると、Fragmentの持つ who
という値と共に親Activityまでリクエストを転送する// Activity.java @Deprecated public void startActivityFromFragment(@NonNull Fragment fragment, @RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) { startActivityForResult(fragment.mWho, intent, requestCode, options); } // https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L5859-L5886
// Activity.java @Override @UnsupportedAppUsage public void startActivityForResult( String who, Intent intent, int requestCode, @Nullable Bundle options) { Uri referrer = onProvideReferrer(); if (referrer != null) { intent.putExtra(Intent.EXTRA_REFERRER, referrer); } options = transferSpringboardActivityOptions(options); Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, who, intent, requestCode, options); if (ar != null) { mMainThread.sendActivityResult( mToken, who, requestCode, ar.getResultCode(), ar.getResultData()); } cancelInputsAndStartExitTransition(options); } // https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L5897-L5919
// Activity.java @UnsupportedAppUsage void dispatchActivityResult(String who, int requestCode, int resultCode, Intent data, String reason) { if (false) Log.v( TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode + ", resCode=" + resultCode + ", data=" + data); mFragments.noteStateNotSaved(); if (who == null) { onActivityResult(requestCode, resultCode, data); } else if (who.startsWith(REQUEST_PERMISSIONS_WHO_PREFIX)) { who = who.substring(REQUEST_PERMISSIONS_WHO_PREFIX.length()); if (TextUtils.isEmpty(who)) { dispatchRequestPermissionsResult(requestCode, data); } else { Fragment frag = mFragments.findFragmentByWho(who); if (frag != null) { dispatchRequestPermissionsResultToFragment(requestCode, data, frag); } } } else if (who.startsWith("@android:view:")) { ArrayList<ViewRootImpl> views = WindowManagerGlobal.getInstance().getRootViews( getActivityToken()); for (ViewRootImpl viewRoot : views) { if (viewRoot.getView() != null && viewRoot.getView().dispatchActivityResult( who, requestCode, resultCode, data)) { return; } } } else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) { Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null; getAutofillManager().onAuthenticationResult(requestCode, resultData, getCurrentFocus()); } else { Fragment frag = mFragments.findFragmentByWho(who); if (frag != null) { frag.onActivityResult(requestCode, resultCode, data); } } writeEventLog(LOG_AM_ON_ACTIVITY_RESULT_CALLED, reason); } // https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L8123-L8162
対してAndroidX側実装だが、そもそもSupport LibraryとしてFragmentの仕組みが利用できない(Platformに含まれない)OSでの利用を想定して作られたものであるため、who値と共に直接dispatchしこれを元に手繰るような手段は取れない。そのためこちらでは、
startActivityForResult
が実行されると、Fragmentのインスタンス共に親Activityまでリクエストを転送するというような感じの実装になっている。
// FragmentActivity.java public void startActivityFromFragment(@NonNull Fragment fragment, @SuppressLint("UnknownNullness") Intent intent, int requestCode, @Nullable Bundle options) { mStartedActivityFromFragment = true; try { if (requestCode == -1) { ActivityCompat.startActivityForResult(this, intent, -1, options); return; } checkForValidRequestCode(requestCode); int requestIndex = allocateRequestIndex(fragment); ActivityCompat.startActivityForResult( this, intent, ((requestIndex + 1) << 16) + (requestCode & 0xffff), options); } finally { mStartedActivityFromFragment = false; } } // https://github.com/aosp-mirror/platform_frameworks_support/blob/b9cd83371e928380610719dfbf97c87c58e80916/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java#L781-L800
// FragmentActivity.java @Override @CallSuper protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { mFragments.noteStateNotSaved(); int requestIndex = requestCode>>16; if (requestIndex != 0) { requestIndex--; String who = mPendingFragmentActivityResults.get(requestIndex); mPendingFragmentActivityResults.remove(requestIndex); if (who == null) { Log.w(TAG, "Activity result delivered for unknown Fragment."); return; } Fragment targetFragment = mFragments.findFragmentByWho(who); if (targetFragment == null) { Log.w(TAG, "Activity result no fragment exists for who: " + who); } else { targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data); } return; } ActivityCompat.PermissionCompatDelegate delegate = ActivityCompat.getPermissionCompatDelegate(); if (delegate != null && delegate.onActivityResult(this, requestCode, resultCode, data)) { // Delegate has handled the activity result return; } super.onActivityResult(requestCode, resultCode, data); } // https://github.com/aosp-mirror/platform_frameworks_support/blob/b9cd83371e928380610719dfbf97c87c58e80916/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java#L149-L182
このとおり、Activity / Fragment両側にresultが通知され、Framework側とは異なる挙動を示すのにはある側面では致し方ない事由が存在するのである。
この仕様によって特段影響があるかというと、多くの場合は影響がない。
AndroidのActivityは、異なるアプリケーションのActivity間(異なるプロセス間)でも相互に連携し動作できる設計思想にある。そのためIntentやrequestCodeとして未知のものが通知される可能性を考慮した実装を行うことを想定している。
事実として(現在は新しいAPIに置き換わったため参照できないが)Android Developersでは自身が処理可能なrequestCodeだけ処理対象にするコードが例示されている。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { // Check which request we're responding to if (requestCode == PICK_CONTACT_REQUEST) { // Make sure the request was successful if (resultCode == Activity.RESULT_OK) { // The user picked a contact. // The Intent's data Uri identifies which contact was selected. // Do something with the contact here (bigger example below) } } } // https://web.archive.org/web/20190703204900if_/https://developer.android.com/training/basics/intents/result?hl=ja#ReceiveResult
すなわち、AndroidX側の親Activityに本来Fragment側で取りたいrequestCode以外が到達していてもこれは無視すればよいし、そのような実装をしている限りFramework API側の実装に切り替わるようなことがあっても問題なく動作するはずだ。