由集成ARouter引发的一些思考

引子

最近打算把项目的各个页面按模块的不同做拆分,也就是简单地想做下组件化的改造吧,那么这样一来不同模块的各个页面就互不依赖了,自然不能直接通过startActivity来显式跳转了,自带的隐式跳转又略显笨重,不够灵活,于是乎就想到了引入路由框架,在github上找找,看到现在用的最多的就是ARouter了吧,看了下主页的介绍,支持的功能还是挺多的,就它了!

因为今天想讲的是页面之间的数据交互,那先来看下ARouter关于这方面的使用方法:

// 构建标准的路由请求,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传递的数据解析这些数据就好了:

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/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对象里:

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)的方式啰。

问题分析

问题是现在我项目里用了两三个Activity,却有几十个Fragment,大量模块间的页面跳转和数据传递都是由Fragment发起的,这样就产生了一个问题,Fragment虽然也有startActivityForResultonActivityResult,但是根据上面对ARouter的源码简单分析来看,我们压根调用的都是它所依附的Activity的这两个方法。
github上的issues49是这么解决的:

  @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方法:

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

那最后我采取这种方案了吗?

image

思考

通过上面的一系列的分析,我其实得到的最有用的信息是,FragmentActivity原来还有这么一个方法:

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

注意这是个public方法,意味着不需要反射就可以调用了,所以我们就能很好地利用它了。

考虑到上面的兼容方法太粗暴了,不够优雅,而且路由本来就是用来解耦代码的,这样处理反而产生了耦合。我那个小项目也不需要ARouter那些拦截器啊,全局降级啊这些高级用法,所以我把ARouter代码下下来,删删减减,并新增了navigation(Fragment mFragment, int requestCode)方法:

if (currentContext is FragmentActivity && fragment != null) {
    currentContext.startActivityFromFragment(fragment, intent, requestCode)
} else {
    startActivity(requestCode, currentContext, intent, routeMeta, callback)
}

应用

可以利用上述方法,抛弃繁琐模板化的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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容