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

时间:2013-07-25 02:00:11

标签: java android illegalstateexception

背景

Android中的异步回调

尝试在Android上以可靠的方式执行异步操作是不必要的复杂,即Is AsyncTask really conceptually flawed or am I just missing something?

现在,这是在引入Fragments之前的全部内容。随着Fragments的引入,onRetainNonConfigurationInstance()已被弃用。所以最新的谷歌纵容黑客是使用一个持久的非UI片段,当配置发生变化时(即旋转屏幕,更改语言设置等),它会从您的Activity中附加/分离。

实施例: 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。片段本身也有一个子片段管理器,用于嵌套片段。遗憾的是,子片段管理器根本不与Activity的片段管理器保持同步。因此,虽然活动的片段管理器是最新的,并且始终具有正确的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

无论如何,有点偏离主题。

死胡同解决方案

所以,您可能认为问题的解决方案很简单,在这一点似乎是一个反义词,为您的孩子添加另一个漂亮的hacky非UI片段片段管理员。据推测,它的回调与其内部状态是同步的,事情都会很花哨。

错了!

Android不支持作为子项附加到其他片段(也称为嵌套片段)的保留片段实例。现在,事后看来这应该是有道理的。如果在活动被销毁时销毁了父片段,因为它未被保留,则无法重新附加子片段。所以这不会起作用。

我的问题

是否有人有一个解决方案来确定何时可以安全地执行子片段上的片段事务以及异步代码回调?

1 个答案:

答案 0 :(得分:4)

更新2

React Native

如果你能忍受它,请使用React Native。我知道,我知道......“肮脏的网络技术”,但严肃地说,Android SDK是一场灾难,所以吞下你的骄傲,只是试一试。你可能会惊讶自己;我知道我做到了!

不能或不会使用React Native

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

而是尝试以下之一:

  1. 转移到基于LocalBroadcastReceiver的简单消息传递系统,并且具有长生命对象(常规Java类或Android服务)在您的应用的本地状态发生变化时执行请求并触发事件。然后在您的活动/片段中,只听取某些Intent并相应地更新。
  2. 使用Reactive事件库(例如RxJava)。我自己并没有在Android上试过这个,但是使用类似的概念库ReactiveCocoa在Mac /桌面应用程序上取得了相当不错的成功。不可否认,这些图书馆的学习曲线相当陡峭,但一旦你习惯了,这种方法就会让人耳目一新。
  3. 更新1:快速和肮脏(官方)解决方案

    我相信这是Google最新的官方解决方案。但是,解决方案确实不能很好地扩展。如果你不熟悉队列,处理程序和保留的实例状态,那么这可能是你唯一的选择...但是不要说我没有警告你!

    Android活动和片段支持可以与LoaderManager一起使用的AsyncTaskLoader。在幕后,加载程序管理器的保留方式与保留的碎片完全相同。因此,这个解决方案确实与我自己的解决方案有一点共同之处。 AsyncTaskLoader是一种部分预先固定的解决方案,在技术上工作。但是,API非常麻烦;我确信你会在几分钟内注意到它。

    我的解决方案

    首先,我的解决方案实施起来并不简单。但是,一旦您的实施工作,使用起来轻而易举,您可以根据自己的内容进行自定义。

    我使用保留的片段添加到Activity的片段管理器(或者在我的案例中支持片段管理器)。这与我的问题中提到的技术相同。此片段充当各种提供程序,用于跟踪它附加到哪个活动,并具有Message和Runnable(实际上是自定义子类)队列。当实例状态不再保存时,队列将执行相应的处理程序(或runnable)“已准备好执行”。

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

    使用某种抽象(如UUID)而不是直接引用使用者(即片段)非常重要。这是因为片段经常被破坏和重新创建,并且您希望您的回调具有对新片段的“引用”;不属于被破坏的活动的旧的。因此,遗憾的是,您很少能安全地使用匿名类捕获的变量。同样,这是因为这些变量可能引用旧的已破坏的片段或活动。相反,您必须向提供程序询问与处理程序已存储的UUID匹配的使用者。然后,您可以将此使用者转换为它实际存在的任何片段/对象并安全地使用它,因为您知道它具有有效Context(活动)的最新片段。

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

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

    这导致处理程序总是使用有效的Context(提供者引用的Activity)和最新的有效Fragment(又名“consumer”)执行。

    结论

    解决方案非常复杂,但是一旦你弄清楚如何实现它,它确实可以正常工作。如果有人想出一个更简单的解决方案,那么我很乐意听到它。