navigator這個坑貨

  1. 使用的replace,所以切換的時候頁面會重新刷新
  2. 使用popBackStack()返回上一級的時候,上一級的頁面會重新刷新,但是全局變量不會重新創(chuàng)建,可用于保存數(shù)據(jù),但不需要保存的數(shù)據(jù)頁會被保存,需要在生命周期里重新初始化
  3. 如果不使用popBackStack()返回上一級,而是使用navigate跳轉(zhuǎn),Android系統(tǒng)執(zhí)行onBackPressed()的時候,會執(zhí)行popBackStack(),所以需要攔截掉并拋出回調(diào),并在回調(diào)中統(tǒng)一執(zhí)行頁面跳轉(zhuǎn)邏輯
  4. popBackStack()返回時,RecyclerView緩存數(shù)據(jù)狀態(tài)的時候,會記錄之前的滾動位置,如下代碼所示的樣式,有當(dāng)前頁前面的數(shù)據(jù)無法緩存
class DemoAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>{
  private val data = mutableListOf<Info>()
  private val positionMap = mutableMapOf<String, Int>()
  fun initData(list:MutableList<Info>){
    positionMap.clear()
    data.addAll(list)
    notifyDataSetChanged()
  }
  
  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    positionMap[data[holder.bindingAdapterPosition].name] = holder.bindingAdapterPosition
  }
}
  1. 謹(jǐn)慎使用lateinit
  2. 進程被殺后,會殘留緩存,在下次開啟時,可能導(dǎo)致狀態(tài)錯亂,需要清理緩存信息
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.clear()
}
  1. 只有popBackStack()會緩存數(shù)據(jù),如果多tab頁切換的情況下只能相互跳轉(zhuǎn),無法緩存數(shù)據(jù),只能通過單例對象(靜態(tài)類)存儲數(shù)據(jù)或狀態(tài)
  2. 多個頁面同時跳轉(zhuǎn)的時候,可能出現(xiàn)崩潰(連續(xù)兩次調(diào)用navigate就可以觸發(fā)),要么在頁面跳轉(zhuǎn)處做多頁面見的防連點,要么在頁面跳轉(zhuǎn)的時候try-catch
  3. 處于后臺的時候切換頁面不執(zhí)行
    場景:初始化數(shù)據(jù),判定跳轉(zhuǎn)到首頁的哪個tab,在調(diào)用數(shù)據(jù)初始化接口的時候,切到后臺。等待初始化接口返回結(jié)果,本該navigator加載頁面的log打印了,但是將應(yīng)用切到前臺的時候,發(fā)現(xiàn)頁面并沒有加載出來
  4. 眾所周知,google出的Navigator各種異常,今天有幸遇到個新花樣。展示DialogFragment的同時開啟了二級頁面,當(dāng)二級頁面返回的時候會崩潰。
java.lang.IllegalStateException: DialogFragment can not be attached to a container view
    at androidx.fragment.app.DialogFragment$4.onChanged(DialogFragment.java:151)
    at androidx.fragment.app.DialogFragment$4.onChanged(DialogFragment.java:144)
    at androidx.lifecycle.LiveData.considerNotify(LiveData.java:133)
    at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:151)
    at androidx.lifecycle.LiveData.setValue(LiveData.java:309)
    at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
    at androidx.fragment.app.Fragment.performCreateView(Fragment.java:3115)
    at androidx.fragment.app.DialogFragment.performCreateView(DialogFragment.java:510)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:524)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
    at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1433)
    at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2977)
    at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:2888)
    at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3129)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
    at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1433)
    at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2977)
    at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:2888)
    at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3129)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
    at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1433)
    at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2977)
    at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:2888)
    at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3129)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1890)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1823)
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1760)
    at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:547)
    at android.os.Handler.handleCallback(Handler.java:938)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:223)
    at android.app.ActivityThread.main(ActivityThread.java:7664)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:607)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)

粗略一看,DialogFragment崩了,而且崩潰的日志是純原生的,這是什么鬼。。。

private Observer<LifecycleOwner> mObserver = new Observer<LifecycleOwner>() {
    @SuppressLint("SyntheticAccessor")
    @Override
    public void onChanged(LifecycleOwner lifecycleOwner) {
        if (lifecycleOwner != null && mShowsDialog) {
            View view = requireView();
            if (view.getParent() != null) {
                throw new IllegalStateException(
                        "DialogFragment can not be attached to a container view");
            }
            if (mDialog != null) {
                if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
                    Log.d(TAG, "DialogFragment " + this + " setting the content view on "
                            + mDialog);
                }
                mDialog.setContentView(view);
            }
        }
    }
};

竟然是生命周期監(jiān)聽導(dǎo)致的崩潰,可二級頁面關(guān)閉,重新回顯當(dāng)前頁面,DialogFragment走生命周期很正常啊,為啥要驗證view.getParent(),而且如果這么操作真的是問題,那google早被人沖了,怎么會等到現(xiàn)在被我發(fā)現(xiàn)。
求助大神同事幫忙排查,原因是返回的時候DialogFragment執(zhí)行了onCreateView方法,而return的view竟然有parent了。
可DialogFragment已經(jīng)展示了,為什么會再次執(zhí)行onCreateView。而且只有當(dāng)從二級頁返回的時候會觸發(fā)異常,但是home鍵回到桌面,再次打開應(yīng)用的時候不會。
經(jīng)過排查,原來是navigator在跳轉(zhuǎn)二級頁的時候,殺死了當(dāng)前Fragment,當(dāng)返回到當(dāng)前頁的時候,重新創(chuàng)建導(dǎo)致的。又由于navigator有緩存,所以新建的DialogFragment也會緩存對應(yīng)的parent信息。
無奈,只能在onCreateView添加處理邏輯,雖然不夠優(yōu)雅,但至少好用吧。

 override fun onCreateView(
     inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
 ): View {
     if (mBinding.root.parent != null) {
         (mBinding.root.parent as? ViewGroup)?.removeView(mBinding.root)
     }
     return mBinding.root
 }

…………………………………………
Android的坑千千萬,navigator占一半,繼上次dialog崩潰后,再次遇到navigator + DialogFragment的組合套餐

問題一:

前提:navigator頁面跳轉(zhuǎn)(A -> B),DialogFragment輪詢請求網(wǎng)絡(luò)并依據(jù)請求結(jié)果進行狀態(tài)變更,且A、B頁面均彈出相同DialogFragment,請求信息一致
手順:A頁面雙指按住B頁面的跳轉(zhuǎn)按鈕和DialogFragment的彈出按鈕,DialogFragmen按鈕稍微早于頁面跳轉(zhuǎn)按鈕抬起(感覺上是先抬起,視覺上基本看不出的時間差),讓DialogFragmen彈出瞬間切換到B頁面,然后在B頁面喚起DialogFragmen,在DialogFragmen展示后,通過外部觸發(fā)DialogFragmen請求結(jié)果的UI變更條件
現(xiàn)象:B頁面UI無變化,關(guān)閉B頁面的DialogFragmen,再關(guān)閉B頁面,發(fā)現(xiàn)A頁面的UI變更
分析:
A頁面DialogFragmen的UI變更:網(wǎng)絡(luò)請求輪詢DialogFragmen持有頁面A,導(dǎo)致navigator沒有成功回收頁面A,導(dǎo)致DialogFragmen依然能夠接受到網(wǎng)絡(luò)請求數(shù)據(jù),執(zhí)行頁面UI切換。
B頁面DialogFragmen無響應(yīng),著急修改更嚴(yán)重的問題,沒進一步分析

問題二:

前提:navigator頁面跳轉(zhuǎn)(A -> B),DialogFragment延遲關(guān)閉。
手順:A頁面雙指按住B頁面的跳轉(zhuǎn)按鈕和DialogFragment的彈出按鈕,DialogFragmen按鈕稍微早于頁面跳轉(zhuǎn)按鈕抬起(感覺上是先抬起,視覺上基本看不出的時間差),讓DialogFragmen彈出瞬間切換到B頁面,且DialogFragmen的延遲關(guān)閉邏輯是在切換到B頁面后執(zhí)行
現(xiàn)象:從B頁面返回到A頁面,本應(yīng)該關(guān)閉的彈窗被再次展示出來,且不會自動關(guān)閉,點擊關(guān)閉按鈕有按下態(tài)效果,但彈窗依然不關(guān)閉
分析:

  1. 通過DialogFragment,onCreateView時候打印的hashcode可知,再次展示的DialogFragment與被異步關(guān)閉的DialogFragment是同一個
  2. 通過點擊事件斷點可知,點擊事件確實有響應(yīng)
  3. 代碼分析如下:


    調(diào)用dialog的dismiss

    mDismissed為true

不知道navigator在展示的時候是怎么創(chuàng)建的:


FragmentStateManager

沒有執(zhí)行show


show

沒有執(zhí)行showNow
showNow

也沒有執(zhí)行onAttach


onAttach

…………………………………………
不用千年等一回,bug又來嘞?。。?br> 當(dāng)前頁面結(jié)構(gòu)如圖:

MainActivity
├── Fragment A
│   ├── Fragment A1
│   └── Fragment A2
└── Fragment B

且根據(jù)產(chǎn)品需求,在頁面跳轉(zhuǎn)時需要加入動畫效果:navigator.xml

<fragment
        android:id="@+id/fragmentA"
        android:name="com.demo.ui.fragment.AFragment"
        android:label="fragmentA"
        tools:layout="@layout/fragmentA">
        <action
            android:id="@+id/fragment_a_to_fragment_b"
            app:destination="@id/fragmentB"
            app:enterAnim="@anim/anim_page_in"
            app:exitAnim="@anim/anim_page_out"
            app:popEnterAnim="@anim/anim_page_in"
            app:popExitAnim="@anim/anim_page_out" />
</fragment>
<fragment
        android:id="@+id/fragmentB"
        android:name="com.demo.ui.fragment.BFragment"
        android:label="BFragment">
</fragment>

anim_page_in.xml

<?xml version="1.0" encoding="utf-8"?>
<alpha android:fromAlpha="0"
    android:toAlpha="1"
    android:duration="500"
    xmlns:android="http://schemas.android.com/apk/res/android" />

anim_page_out.xml

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:fromAlpha="1"
    android:toAlpha="0" />

原本只是完成了這個需求,結(jié)果測試的時候,多指操作,在Fragment A1中通過navigation的原生跳轉(zhuǎn)方法調(diào)用Fragment A跳轉(zhuǎn)到Fragment B中,并在動畫執(zhí)行過程中,再次點擊Fragment A1的另一個button,執(zhí)行Fragment A1跳轉(zhuǎn)到Fragment A2的操作。
當(dāng)執(zhí)行完成后,可以看到應(yīng)用當(dāng)前展示的是Fragment B的頁面,但當(dāng)執(zhí)行系統(tǒng)的虛擬返回操作時,F(xiàn)ragment B卻沒有監(jiān)聽到onBackPressed(),而是Fragment A2監(jiān)聽到了onBackPressed(),由于在Fragment A2的onBackPressed()執(zhí)行了如下方法:

override fun onBackPressed() {
    super.onBackPressed()
    activity?.onBackPressed()
}

導(dǎo)致頁面后臺運行,當(dāng)將應(yīng)用喚到前臺的時候,再次點擊Fragment A1調(diào)用Fragment A跳轉(zhuǎn)Fragment B的按鈕的時候,發(fā)現(xiàn)Fragment A1無法找到parentFragment的方式找到Fragment A。暫未發(fā)現(xiàn)官方可用方案,暫時只能在執(zhí)行跳轉(zhuǎn)時攔截點擊事件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容