一尘不染

嵌套片段-IllegalStateException“在onSaveInstanceState之后无法执行此操作”

java

背景

Android中的异步回调

试图在Android上以可靠的方式执行异步操作是不必要的麻烦,即AsyncTask确实在概念上存在缺陷吗?还是我只是缺少一些东西?

现在,一切都在引入片段之前。随着Fragments的引入,onRetainNonConfigurationInstance()已被弃用。因此,最新的Google宽容黑客手段是使用永久性的非UI片段,该片段会在配置发生变化(例如,旋转屏幕,更改语言设置等)时与您的活动相关联/分离。

范例:https
//code.google.com/p/android/issues/detail?id =
23096#c4

IllegalStateException-在onSaveInstanceState之后无法执行此操作

从理论上讲,上面的技巧可以使您摆脱恐惧:

IllegalStateException - "Can not perform this action after onSaveInstanceState"

因为一个持久的非UI片段将接收onViewStateRestored()(或者onResume)和onSaveInstanceState()(或者onPause)的回调。这样,您就可以知道何时保存/恢复实例状态。对于这么简单的事情,这是相当不错的代码,但是利用这些知识,您可以将异步回调排队,直到活动的FragmentManager在执行它们之前将其mStateSaved变量设置为false。

mStateSaved是最终负责引发此异常的变量。

private void checkStateLoss() {
    if (mStateSaved) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
        throw new IllegalStateException(
                "Can not perform this action inside of " + mNoTransactionsBecause);
    }
}

因此,从理论上讲,现在您知道何时可以安全地执行片段事务,因此可以避免可怕的IllegalStateException。

错误!

嵌套片段

上面的解决方案仅适用于Activity的FragmentManager。片段本身还具有子片段管理器,该子片段管理器用于嵌套片段。不幸的是,子片段管理器根本没有与活动的片段管理器保持同步。因此,尽管活动的片段管理器是最新的,并且始终具有正确的mStateSaved;子片段会另外考虑,并会在不适当的时间高兴地引发可怕的IllegalStateException。

现在,如果您在支持库中查看了Fragment.java和FragmentManager.java,那么与片段有关的所有事情都容易出错,您将不会感到惊讶。设计和代码质量是…啊,值得怀疑。你喜欢布尔吗?

mHasMenu = false
mHidden = false
mInLayout = false
mIndex = 1
mFromLayout = false
mFragmentId = 0
mLoadersStarted = true
mMenuVisible = true
mNextAnim = 0
mDetached = false
mRemoving = false
mRestored = false
mResumed = true
mRetainInstance = true
mRetaining = false
mDeferStart = false
mContainerId = 0
mState = 5
mStateAfterAnimating = 0
mCheckedForLoaderManager = true
mCalled = true
mTargetIndex = -1
mTargetRequestCode = 0
mUserVisibleHint = true
mBackStackNesting = 0
mAdded = true

无论如何,摆脱话题。

死胡同

因此,您可能会认为问题的解决方案很简单, 这似乎有点像是反义词,
即向您的子片段管理器中添加另一个漂亮的非UI片段。大概它的回调与它的内部状态是同步的,一切都会变得花花公子。

又错了!

Android不支持将保留的片段实例作为子片段附加到其他片段(也称为嵌套片段)。现在,事后看来,这应该是有道理的。如果由于活动未保留而在活动被销毁时父片段被销毁,则无处可以重新附加子片段。所以这只是行不通的。

我的问题

是否有人可以通过异步代码回调来确定何时可以安全地 对子片段 执行片段事务?


阅读 175

收藏
2020-12-03

共1个答案

一尘不染

更新2

反应本机

如果可以忍受,请使用React Native。我知道,我知道…“肮脏的网络技术”,但说真的,Android
SDK简直是一场灾难,所以请放下你的骄傲,随它去吧。您可能会感到惊讶;我知道我做到了!

无法或不会使用React Native

不用担心,我建议从根本上改变您的联网方法。触发请求并运行请求处理程序以更新UI不适用于Android的组件生命周期。

而是尝试以下方法之一:

  1. 转向基于的简单消息传递系统,LocalBroadcastReceiver当应用程序的本地状态更改时,具有长寿命的对象(常规Java类或Android服务)会处理您的请求并触发事件。然后在“活动/片段”中,听某些声音Intent并相应地进行更新。
  2. 使用反应式事件库(例如RxJava)。我没有在Android上尝试过此操作,但使用类似的概念库ReactiveCocoa(用于Mac /桌面应用程序)获得了相当不错的成功。诚然,这些库的学习曲线相当陡峭,但是一旦您习惯了这种方法,它就会令人耳目一新。

更新1:快速和肮脏(官方)解决方案

我相信这是Google的最新官方解决方案。但是,该解决方案确实无法很好地扩展。如果您不习惯自己弄乱队列,处理程序和保留实例状态,那么这可能是您唯一的选择……但是请不要说我没有警告您!

Android活动和片段支持可与AsyncTaskLoader一起使用的LoaderManager。在后台,装载程序管理器的保留方式与保留的片段完全相同。因此,此解决方案与下面的我自己的解决方案确实有一些共同点。AsyncTaskLoader是一种部分罐头解决方案,在技术上可以
正常工作
。但是,API非常麻烦。我相信您会在使用它的几分钟后注意到它。
__

我的解决方案

首先,我的解决方案绝非易事。但是,一旦实现工作顺利进行,就很容易使用,并且可以根据自己的需求对其进行自定义。

我使用保留的片段,该片段被添加到“活动”的片段管理器中(或者在我的情况下支持片段管理器)。这是我的问题中提到的相同技术。该片段充当各种 提供程序
,可跟踪其附加到的活动,并具有Message和Runnable(实际上是自定义子类)队列。当不再保存实例状态 并且
相应的处理程序(或可运行的)“准备执行” 时,将执行队列。

每个处理程序/可运行程序都存储一个引用 使用者 的UUID 。 消费者 通常是活动中某处的碎片(可以安全地嵌套)。当 使用者
片段附加到活动时,它将查找 提供者 片段并使用其UUID进行注册。

使用某种抽象(例如UUID),而不是直接引用 使用者
(即片段),这一点很重要。这是因为片段经常被破坏和重新创建,并且您希望回调具有对新片段的“引用”。不是属于被破坏活动的旧物品。因此,不幸的是,您很少可以安全地使用匿名类捕获的变量。同样,这是因为这些变量可能引用了一个已损坏的旧片段或活动。相反,你必须要求
供应商消费者 处理程序已存储的UUID相匹配。然后,您可以投放此 消费者
到它实际存在的任何片段/对象并安全地使用它,因为您知道它的最新片段具有有效的Context(活动)。

使用者 (由UUID引用)准备就绪时,处理程序(或可运行)将“准备执行” 。除了 提供者 之外,还需要检查 使用者
是否准备就绪,因为正如我的问题中所述,即使提供者另有说明,使用者片段也可能认为其实例状态已保存。如果使用者(或提供者)尚未准备就绪,则可以将消息(或可运行)放入提供者的队列中。
__

使用者 片段到达onResume()时,它通知 提供者 它已准备好使用已排队的消息/可运行对象。此时, 提供者
可以尝试执行其队列中属于准备就绪的 使用者的 任何操作。

这导致处理程序始终使用有效的上下文(提供者引用的活动)和最新的有效片段(也称为“消费者”)执行。

结论

该解决方案非常复杂,但是一旦确定了实现方法,它就可以完美地工作。如果有人提出了一个更简单的解决方案,那么我很乐意听到。

2020-12-03