primers

Recently, I plan to split each page of the project according to different modules, that is to say, I simply want to do the componentized transformation, so that the pages of different modules are not dependent on each other, naturally, they cannot directly jump through startActivity explicitly, and the implicit jump is a little cumbersome and not flexible enough. As a result, I thought of the introduction of routing framework, github search, see now use the most is ARouter bar, see the introduction of the home page, support the function or quite many, it!

Since we want to talk about data interaction between pages today, let’s take a look at ARouter’s approach to this:

// Build the standard routing request, startActivityForResult
// The first parameter of navigation must be Activity and the second parameter must be RequestCode
ARouter.getInstance().build("/test/1")
            .withLong("key1".666L)
            .withString("key3"."888")
            .withObject("key4".new Test("Jack"."Rose"))
            .navigation(this.5);
Copy the code

Then parse the data in the corresponding Activity like the data passed by startActivity:

// Add annotations to pages that support routing (mandatory)
// The path must have at least two levels, /xx/xx
@Route(path = "/test/activity")
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bundle bundle = getIntent().getExtras();
        if(bundle ! =null) {
            Long key1 = bundle.getLong("key1"); }}}Copy the code

ARouter uses withXXX to store all the data in the mBundle object:

public Postcard withString(@Nullable String key, @Nullable String value) {
        mBundle.putString(key, value);
        return this;
    }
public Bundle getExtras(a) {
        return mBundle;
    }
Copy the code

In the Intent object:

// Build intent
 final Intent intent = new Intent(currentContext, postcard.getDestination());
 intent.putExtras(postcard.getExtras());
 ....//省略
 ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
Copy the code

In the end, the normal startActivityForResult is called to jump to the page and pass the data. So how do you return data to the next page? It is the same as setResult(int resultCode, Intent data).

Problem analysis

The problem is that now I have used two or three activities in my project, but there are dozens of fragments. A large number of page jumps and data transfers between modules are initiated by fragments, which leads to a problem. The Fragment also has startActivityForResult and onActivityResult, but according to ARouter’s source code, we call both methods of the Activity that it is attached to. Issues49 on Github solves it this way:

  @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        List<Fragment> allFragments = getSupportFragmentManager().getFragments();
        if(allFragments ! =null) {
            for(Fragment fragment : allFragments) { fragment.onActivityResult(requestCode, resultCode, data); }}}Copy the code

Manually passing data from the Activity’s onActivityResult to the fragment is simple and crude, All fragments attached to the Acttivty will receive data, and then determine the requestCode and resultCode in the corresponding Fragment.

Source code analysis

To solve this problem, let’s analyze the Fragment startActivityForResult and onActivityResult.

startActivityForResult

   public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent,
            int requestCode, @Nullable Bundle options) {
        if (mHost == null) {
            throw new IllegalStateException("Fragment " + this + " not attached to Activity");
        }
        mHost.onStartActivityFromFragment(this /*fragment*/, intent, requestCode, options);
    }
Copy the code

MHost corresponding above is fragments attached FragmentActivity, so they will call to the FragmentActivity startActivityFromFragment method:

public void startActivityFromFragment(Fragment fragment, Intent intent,
        int requestCode, @Nullable Bundle options) {.../ / to omit
            // Check that the size of the requestCode cannot exceed 0xFFFF
            checkForValidRequestCode(requestCode);
            // Assign a unique requestIndex to the Fragment. Based on this requestIndex, the Fragment's unique identifier mWho can be obtained
            int requestIndex = allocateRequestIndex(fragment);
            // Then call the activity's startActivityForResult
            ActivityCompat.startActivityForResult(
                    this, intent, ((requestIndex + 1) < <16) + (requestCode & 0xffff), options);
}
Copy the code

Each Fragment has a unique identifier field inside who, All call startActivityFromFragment method in FragmentActivity fragments requestCode and who through key – value stored in the mPendingFragmentActivityResu LTS variable

// Allocates the next available startActivityForResult request index. private int allocateRequestIndex(@NonNull Fragment Fragment) {// Find an unallocated requestIndexwhile(mPendingFragmentActivityResults.indexOfKey(mNextCandidateRequestIndex) >= 0) { mNextCandidateRequestIndex = (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS; } / / will requestIndex and fragments mWho saved int requestIndex = mNextCandidateRequestIndex; mPendingFragmentActivityResults.put(requestIndex, fragment.mWho); mNextCandidateRequestIndex = (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS;return requestIndex;
    }

Copy the code

MWho is a fragment variable that uniquely identifies an Framgment.

 @NonNull
    String mWho = UUID.randomUUID().toString();
Copy the code

So by calling the Fragment’s startActivityForResult, we generate a requestIndex that maps the Fragment’s mWho. It’s going to call the Fragment’s Ativity startActivityForResult, but it’s not going to call the Fragment’s requestCode, Instead ((requestIndex + 1) << 16) + (requestCode & 0xFFFF)

onActivityResult

Because the Fragment attached to the FragmentActivity’s startActivityForResult, the Fragment does not need to participate in the jump. So the first thing that gets called back is the onActivityResult of this FragmentActivity:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    mFragments.noteStateNotSaved();
    // Parse to get requestIndex
    int requestIndex = requestCode>>16;
    //requestIndex = 0 indicates that no Fragment has initiated the startActivityForResult call
    if(requestIndex ! =0) {
        requestIndex--;
        
        // Get the Fragment's who variable based on the requestIndex
        String who = mPendingFragmentActivityResults.get(requestIndex);
        mPendingFragmentActivityResults.remove(requestIndex);
        if (who == null) {
            Log.w(TAG, "Activity result delivered for unknown Fragment.");
            return;
        }
        
        // Then fetch the target Fragment based on the who variable, that is, the Fragment that initiated startActivityForResult
        Fragment targetFragment = mFragments.findFragmentByWho(who);
        if (targetFragment == null) {
            Log.w(TAG, "Activity result no fragment exists for who: " + who);
        } else {
            //// parses the requestCode of the original fragment and finally calls the onActivityResult of the fragment
            targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);
        }
        return; }...super.onActivityResult(requestCode, resultCode, data);
}
Copy the code

The following two situations are summarized:

Fragment.onActivityResult FragmentActivity.onActivityResult
Fragment.startActivityForResult Normal receiving The requestCode is not correct
FragmentActivity.startActivityForResult Can’t receive Normal receiving

So the compatibility method above should be changed to:

  @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        List<Fragment> allFragments = getSupportFragmentManager().getFragments();
        if(allFragments ! =null) {
            for (Fragment fragment : allFragments) {
                fragment.onActivityResult(requestCode& 0xffff, resultCode, data); }}}Copy the code

Did I end up doing this?

thinking

The most useful thing I learned from the above analysis is that FragmentActivity actually has a method like this:

public void startActivityFromFragment( Fragment fragment, Intent intent, int requestCode) {
Copy the code

Note that this is a public method, meaning it can be called without reflection, so we can make good use of it.

Considering that the compatibility approach above is too crude and unelegant, and that routing is designed to decouple code, it creates coupling instead. I didn’t need ARouter for my small project, so I removed the ARouter code and added a navigation(Fragment mFragment, int requestCode) method:

        if (requestCode >= 0) {  // Need start for result
            if (currentContext isFragmentActivity && fragment ! =null) {
                currentContext.startActivityFromFragment(fragment, intent, requestCode)
            } else if (currentContext is Activity) {
                ActivityCompat.startActivityForResult(currentContext, intent, requestCode, null)}else {
                Logs.defaults.e("Must use [navigation(activity, ...)]  to support [startActivityForResult]")}}else {
            ActivityCompat.startActivity(currentContext, intent, null)}Copy the code

application

You can use the above method to discard the tedious template startActivityForResult, onActivityResult, and various codes, add a blank Fragment, and process the returned result as a callback:

object MyRouter {

    private var requestCode = AtomicInteger(1)


    fun navigation(fragmentActivity: FragmentActivity, intent: Intent, callback: (Int, Intent?) -> Unit) {
        val code = requestCode.getAndIncrement()
        val emptyFragment = EmptyFragment()
        emptyFragment.callback=callback
        emptyFragment.requestCode= code
        fragmentActivity.supportFragmentManager.beginTransaction().add(emptyFragment, "$code").commit()
        fragmentActivity.startActivityFromFragment(emptyFragment, intent, code)
    }

    fun navigation(fragment: Fragment, intent: Intent, callback: (Int, Intent?) -> Unit) {
        val code = requestCode.getAndIncrement()
        valemptyFragment = EmptyFragment() emptyFragment.callback=callback emptyFragment.requestCode= code fragment.activity? .startActivityFromFragment(emptyFragment, intent, code) } }class EmptyFragment: Fragment() {

    @IntRange(to = 0xFFFF)
    var requestCode: Int = - 1
    var callback: ((Int, Intent?) -> Unit)? = null


    override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
        super.onActivityResult(requestCode, resultCode, data)

        if (this.requestCode == requestCode) { callback? .invoke(resultCode,data) } activity? .supportFragmentManager? .beginTransaction()? .remove(this@EmptyFragment)? .commit() } }Copy the code

This makes the way we jump and retrieve the returned data simpler and more elegant:

    fun toMain2Activity(a) {
        val intent = Intent(this@MainActivity, Main2Activity::class.java)
        MyRouter.navigation(this, intent) { resultCode, data ->
            Log.d("result"."$resultCode    ${data? .getStringExtra("key1")}")}}Copy the code

I also integrated this method into my abbreviated version of ARouter, which has been uploaded to Github.