navigator这个坑货

  1. 使用的replace,所以切换的时候页面会重新刷新
  2. 使用popBackStack()返回上一级的时候,上一级的页面会重新刷新,但是全局变量不会重新创建,可用于保存数据,但不需要保存的数据页会被保存,需要在生命周期里重新初始化
  3. 如果不使用popBackStack()返回上一级,而是使用navigate跳转,Android系统执行onBackPressed()的时候,会执行popBackStack(),所以需要拦截掉并抛出回调,并在回调中统一执行页面跳转逻辑
  4. popBackStack()返回时,RecyclerView缓存数据状态的时候,会记录之前的滚动位置,如下代码所示的样式,有当前页前面的数据无法缓存
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. 谨慎使用lateinit
  2. 进程被杀后,会残留缓存,在下次开启时,可能导致状态错乱,需要清理缓存信息
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.clear()
}
  1. 只有popBackStack()会缓存数据,如果多tab页切换的情况下只能相互跳转,无法缓存数据,只能通过单例对象(静态类)存储数据或状态
  2. 多个页面同时跳转的时候,可能出现崩溃(连续两次调用navigate就可以触发),要么在页面跳转处做多页面见的防连点,要么在页面跳转的时候try-catch
  3. 处于后台的时候切换页面不执行
    场景:初始化数据,判定跳转到首页的哪个tab,在调用数据初始化接口的时候,切到后台。等待初始化接口返回结果,本该navigator加载页面的log打印了,但是将应用切到前台的时候,发现页面并没有加载出来
  4. 众所周知,google出的Navigator各种异常,今天有幸遇到个新花样。展示DialogFragment的同时开启了二级页面,当二级页面返回的时候会崩溃。
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);
            }
        }
    }
};

竟然是生命周期监听导致的崩溃,可二级页面关闭,重新回显当前页面,DialogFragment走生命周期很正常啊,为啥要验证view.getParent(),而且如果这么操作真的是问题,那google早被人冲了,怎么会等到现在被我发现。
求助大神同事帮忙排查,原因是返回的时候DialogFragment执行了onCreateView方法,而return的view竟然有parent了。
可DialogFragment已经展示了,为什么会再次执行onCreateView。而且只有当从二级页返回的时候会触发异常,但是home键回到桌面,再次打开应用的时候不会。
经过排查,原来是navigator在跳转二级页的时候,杀死了当前Fragment,当返回到当前页的时候,重新创建导致的。又由于navigator有缓存,所以新建的DialogFragment也会缓存对应的parent信息。
无奈,只能在onCreateView添加处理逻辑,虽然不够优雅,但至少好用吧。

 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页面跳转(A -> B),DialogFragment轮询请求网络并依据请求结果进行状态变更,且A、B页面均弹出相同DialogFragment,请求信息一致
手顺:A页面双指按住B页面的跳转按钮和DialogFragment的弹出按钮,DialogFragmen按钮稍微早于页面跳转按钮抬起(感觉上是先抬起,视觉上基本看不出的时间差),让DialogFragmen弹出瞬间切换到B页面,然后在B页面唤起DialogFragmen,在DialogFragmen展示后,通过外部触发DialogFragmen请求结果的UI变更条件
现象:B页面UI无变化,关闭B页面的DialogFragmen,再关闭B页面,发现A页面的UI变更
分析:
A页面DialogFragmen的UI变更:网络请求轮询DialogFragmen持有页面A,导致navigator没有成功回收页面A,导致DialogFragmen依然能够接受到网络请求数据,执行页面UI切换。
B页面DialogFragmen无响应,着急修改更严重的问题,没进一步分析

问题二:

前提:navigator页面跳转(A -> B),DialogFragment延迟关闭。
手顺:A页面双指按住B页面的跳转按钮和DialogFragment的弹出按钮,DialogFragmen按钮稍微早于页面跳转按钮抬起(感觉上是先抬起,视觉上基本看不出的时间差),让DialogFragmen弹出瞬间切换到B页面,且DialogFragmen的延迟关闭逻辑是在切换到B页面后执行
现象:从B页面返回到A页面,本应该关闭的弹窗被再次展示出来,且不会自动关闭,点击关闭按钮有按下态效果,但弹窗依然不关闭
分析:

  1. 通过DialogFragment,onCreateView时候打印的hashcode可知,再次展示的DialogFragment与被异步关闭的DialogFragment是同一个
  2. 通过点击事件断点可知,点击事件确实有响应
  3. 代码分析如下:


    调用dialog的dismiss

    mDismissed为true

不知道navigator在展示的时候是怎么创建的:


FragmentStateManager

没有执行show


show

没有执行showNow
showNow

也没有执行onAttach


onAttach

…………………………………………
不用千年等一回,bug又来嘞!!!
当前页面结构如图:

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

且根据产品需求,在页面跳转时需要加入动画效果: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" />

原本只是完成了这个需求,结果测试的时候,多指操作,在Fragment A1中通过navigation的原生跳转方法调用Fragment A跳转到Fragment B中,并在动画执行过程中,再次点击Fragment A1的另一个button,执行Fragment A1跳转到Fragment A2的操作。
当执行完成后,可以看到应用当前展示的是Fragment B的页面,但当执行系统的虚拟返回操作时,Fragment B却没有监听到onBackPressed(),而是Fragment A2监听到了onBackPressed(),由于在Fragment A2的onBackPressed()执行了如下方法:

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

导致页面后台运行,当将应用唤到前台的时候,再次点击Fragment A1调用Fragment A跳转Fragment B的按钮的时候,发现Fragment A1无法找到parentFragment的方式找到Fragment A。暂未发现官方可用方案,暂时只能在执行跳转时拦截点击事件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容