由集成ARouter引起的一些思考

引子

最近打算把項目的各個頁面按模塊的不一樣作拆分,也就是簡單地想作下組件化的改造吧,那麼這樣一來不一樣模塊的各個頁面就互不依賴了,天然不能直接經過startActivity來顯式跳轉了,自帶的隱式跳轉又略顯笨重,不夠靈活,因而乎就想到了引入路由框架,在github上找找,看到如今用的最多的就是ARouter了吧,看了下主頁的介紹,支持的功能仍是挺多的,就它了!java

由於今天想講的是頁面之間的數據交互,那先來看下ARouter關於這方面的使用方法:git

// 構建標準的路由請求,startActivityForResult
// navigation的第一個參數必須是Activity,第二個參數則是RequestCode
ARouter.getInstance().build("/test/1")
            .withLong("key1", 666L)
            .withString("key3", "888")
            .withObject("key4", new Test("Jack", "Rose"))
            .navigation(this, 5);
複製代碼

而後在對應的Activity中像解析startActivity傳遞的數據解析這些數據就行了:github

// 在支持路由的頁面上添加註解(必選)
// 這裏的路徑須要注意的是至少須要有兩級,/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");
        }
    }
}
複製代碼

看過源碼就很簡單了,之因此是這麼作是由於ARouter只是用上面的withXXX幫咱們把數據都存儲到了mBundle對象裏:bash

public Postcard withString(@Nullable String key, @Nullable String value) {
        mBundle.putString(key, value);
        return this;
    }
public Bundle getExtras() {
        return mBundle;
    }
複製代碼

最終塞到了Intent對象裏:框架

// Build intent
 final Intent intent = new Intent(currentContext, postcard.getDestination());
 intent.putExtras(postcard.getExtras());
 ....//省略
 ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
複製代碼

其實最終就是調用普通的startActivityForResult來作頁面跳轉和傳遞數據的。那怎麼返回數據給上一層頁面呢?固然也就是同樣用setResult(int resultCode, Intent data)的方式囉。dom

問題分析

問題是如今我項目裏用了兩三個Activity,卻有幾十個Fragment,大量模塊間的頁面跳轉和數據傳遞都是由Fragment發起的,這樣就產生了一個問題,Fragment雖然也有startActivityForResultonActivityResult,可是根據上面對ARouter的源碼簡單分析來看,咱們壓根調用的都是它所依附的Activity的這兩個方法。 github上的issues49是這麼解決的:ide

@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);
            }
        }
    }
複製代碼

手動把數據從Activity的onActivityResult傳遞到fragment裏,這樣簡單粗暴,全部attach到這個Acttivty的Fragment都會收到數據,固然再在對應的Fragment裏判斷requestCoderesultCode,這樣就沒問題了嗎?源碼分析

源碼分析

要解決這個問題,咱們來分析下FragmentstartActivityForResultonActivityResult組件化

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);
    }
複製代碼

上面的mHost對應的就是Fragment依附的FragmentActivity,因此會調用到這個FragmentActivitystartActivityFromFragment方法:post

public void startActivityFromFragment(Fragment fragment, Intent intent, int requestCode, @Nullable Bundle options) {
            ....//省略
            //檢查requestCode大小,不能超過0xffff
            checkForValidRequestCode(requestCode);
            //分配給這個Fragment惟一的requestIndex,根據這個requestIndex能夠獲取到對應Fragment的惟一標識mWho
            int requestIndex = allocateRequestIndex(fragment);
            //以後就調用activity的startActivityForResult
            ActivityCompat.startActivityForResult(
                    this, intent, ((requestIndex + 1) << 16) + (requestCode & 0xffff), options);
}
複製代碼

每個Fragment在內部都有一個惟一的標識字段who,在FragmentActivity中把全部調用startActivityFromFragment方法的fragment的requestCodewho經過key-value的方式保存在mPendingFragmentActivityResults變量中

// Allocates the next available startActivityForResult request index.
    private int allocateRequestIndex(@NonNull Fragment fragment) {
      
        //找到一個還沒有分配的requestIndex
        while (mPendingFragmentActivityResults.indexOfKey(mNextCandidateRequestIndex) >= 0) {
            mNextCandidateRequestIndex =
                    (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS;
        }
        //將requestIndex和fragment的mWho保存起來
        int requestIndex = mNextCandidateRequestIndex;
        mPendingFragmentActivityResults.put(requestIndex, fragment.mWho);
        mNextCandidateRequestIndex =
                (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS;

        return requestIndex;
    }

複製代碼

mWho是fragment一個變量,用來惟一標識一個Framgment。

@NonNull
    String mWho = UUID.randomUUID().toString();
複製代碼

因此經過調用FragmentstartActivityForResult,咱們會生成一個requestIndex,來和fragment的mWho創建映射關係,至此Fragment對象的任務就完成了,而後調用的就是Ativity的startActivityForResult了,不過它的requestCode也不是Fragment的requestCode,而是((requestIndex + 1) << 16) + (requestCode & 0xffff)

onActivityResult

由於最終調用的是發起跳轉的Fragment所attach的FragmentActivitystartActivityForResult,只是requestCode作了特殊處理了而已,Fragment並不須要參與跳轉,因此最早被回調的也就是這個FragmentActivityonActivityResult

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    mFragments.noteStateNotSaved();
    //解析獲得requestIndex
    int requestIndex = requestCode>>16;
    //requestIndex = 0就表示沒有Fragment發起過startActivityForResult調用
    if (requestIndex != 0) {
        requestIndex--;
        
        //根據requestIndex獲取Fragment的who變量
        String who = mPendingFragmentActivityResults.get(requestIndex);
        mPendingFragmentActivityResults.remove(requestIndex);
        if (who == null) {
            Log.w(TAG, "Activity result delivered for unknown Fragment.");
            return;
        }
        
        //而後根據who變量獲取目標Fragment,也就是發起startActivityForResult的那個`fragment`
        Fragment targetFragment = mFragments.findFragmentByWho(who);
        if (targetFragment == null) {
            Log.w(TAG, "Activity result no fragment exists for who: " + who);
        } else {
            ////解析獲得最初fragment的requestCode,最後調用Fragment的onActivityResult
            targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);
        }
        return;
    }

    ...
    super.onActivityResult(requestCode, resultCode, data);
}
複製代碼

下面總結下兩種狀況表現:

Fragment.onActivityResult FragmentActivity.onActivityResult
Fragment.startActivityForResult 正常接收 異常接收,requestCode不對
FragmentActivity.startActivityForResult 不能接收 正常接收

因此上面的兼容方法應該改爲:

@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);
            }
        }
    }
複製代碼

那最後我採起這種方案了嗎?

思考

經過上面的一系列的分析,我其實獲得的最有用的信息是,FragmentActivity原來還有這麼一個方法:

public void startActivityFromFragment( Fragment fragment, Intent intent, int requestCode) {
複製代碼

注意這是個public方法,意味着不須要反射就能夠調用了,因此咱們就能很好地利用它了。

考慮到上面的兼容方法太粗暴了,不夠優雅,並且路由原本就是用來解耦代碼的,這樣處理反而產生了耦合。我那個小項目也不須要ARouter那些攔截器啊,全局降級啊這些高級用法,因此我把ARouter代碼下下來,刪刪減減,並新增了navigation(Fragment mFragment, int requestCode)方法:

if (requestCode >= 0) {  // Need start for result
            if (currentContext is FragmentActivity && 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)
        }
複製代碼

應用

能夠利用上述方法,拋棄繁瑣模板化的startActivityForResultonActivityResult和各類code,添加一個空白的Fragment,並採用回調的方式處理返回結果:

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()
        val emptyFragment = 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()
    }
}

複製代碼

這樣咱們跳轉和拿到返回數據的方式也就變得比較簡潔和優雅了:

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

順手也把這種方式的跳轉整合到了個人縮減版ARouter中了,代碼已傳到github。

相關文章
相關標籤/搜索