如何在Android中使用Fragment执行后台操作并正确更新UI?

3

我找不到任何解决方案能够满足我所有的要求。

在我的应用程序中,我使用 AsyncTasks 执行一些操作,例如将数据保存到内存或从数据库读取数据。我在 onPreExecute 中创建进度对话框,在 onProgressUpdate 中更新进度值,并在 onPostExecute 中关闭对话框。

最近我切换到 Fragment API(我使用支持库来针对旧版本的 Android),这意味着我的活动子类化了 FragmentActivity,而对话框则子类化了 DialogFragment

切换到 Fragment API 导致一个众所周知的问题 - 有时我会遇到以下异常:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

例如,当用户启动后台操作(进度对话框出现),使用主页按钮最小化应用程序并在活动处于后台时完成操作时,就会发生这种情况。然后应用程序尝试解除对话框,但由于活动的状态已保存,因此失败了。
我理解这个问题。可以通过确保将对UI的更改推迟到恢复活动时来修复它,如本文所述:如何处理活动/片段暂停时的处理程序消息
然而,这种解决方案会导致另一个问题。如果操作在活动处于后台时完成,稍后Android杀死了该活动,那么怎么办?当用户返回应用程序时,它会恢复其保存在onSaveInstanceState中的状态。因此,进度对话框仍然可见,并显示与活动放入后台时相同的进度值。应该解除它的消息从未被处理,当活动被杀死时就丢失了。
如何正确解决所有描述的问题?如何允许在活动处于后台时更改UI,或者至少允许推迟UI更改并确保它们不会在Android杀死活动时丢失?该解决方案必须允许跟踪后台任务的进度。

如果DialogFragmentFragment重新实例化时被恢复,则只需检查操作是否仍在运行,如果没有,则关闭DialogFragment。如果您没有持久化您的任务,那么在进程重新启动后无论如何都不会运行。 - corsair992
2个回答

2
避免描述中提到的情况的唯一可靠方法是将结果持久化到某种存储器中。您可以启动一个服务来执行长时间运行的任务,并将结果写入数据库,同时在完成时发送本地广播。
如果活动在前台,则会接收广播并更新其状态以知道进程已完成,并且可以从使用的任何持久性存储机制中删除有关任务的信息。
如果活动在后台,则服务将完成、发送广播并保留结果。下次活动进入前台时,它应该检查此持久性存储并查看是否存在未解决的任务状态,并更新其状态以匹配。
因此,一个例子可能是(让我们使用“上传照片”作为任务):
1. 用户点击按钮开始上传图像 2. 活动启动服务将图像上传到远程服务 3. 活动为您定义的某些广播事件注册LocalBroadcastManager(例如com.mypackage.ACTION_PHOTO_UPlOADED) 4. 服务保存有关任务的某些信息(例如SharedPreferences.putBoolean("TASK_PHOTO_UPLOADED", false)) 5. 活动在服务处理上传任务时显示某种加载UI 6. 用户按Home键并将应用程序发送到停止状态 7. 活动取消注册本地广播管理器 8. 服务完成上传图像 9. 服务发送本地广播(com.mypackage.ACTION_PHOTO_UPlOADED),但因为Activity不再侦听而未使用 10. 服务更新持久化信息(SharedPreferences.putBoolean("TASK_PHOTO_UPLOADED", true)) 11. Android关闭活动并保存其状态 12. 用户稍后返回到活动 13. 活动在onResume()中与服务检查是否有未处理的任务结果(if (SharedPreferences.has("TASK_PHOTO_UPLOADED"))),然后检查是否应该更新状态。 14. 如果照片上传密钥存在且为true,则活动将删除对话框片段并清除SharedPreferences中的TASK_PHOTO_UPLOADED密钥。如果照片上传密钥存在但为false,则活动将注册本地广播管理器以等待事件完成。

1
虽然您使用“Service”和“LocalBroadcastManager”来描述它,而不是使用“AsyncTask”,但是相同的思路可以在这里使用。然而,使用“SharedPreferences”来保留一个信息,即下次创建/恢复活动时是否应该关闭对话框,似乎有点大材小用。我想知道是否有更简单、更优雅的解决方案。 - maral
1
SharedPreferences(就像Service和LocalBroadcastManager一样)只是建议的实现方式。您需要某种形式的持久性存储 - SharedPreferences是最容易展示的,尽管您可以使用数据库(现在这是真正的大炮)。 Service可以用AsyncTask替换(虽然我强烈建议使用Service,因为您不必担心使用Activity生命周期管理它),而LocalBroadcastManager可以被事件总线或类似模式替换。 - Kevin Coppock
2
+1 如果您希望在进程被终止的情况下仍然完成操作,那么“Service”是最好的选择。否则,每当应用程序恢复运行时,您可以忽略“ProgressDialog”,因为无论如何该操作都不会运行。或者,您可能有兴趣使用一些第三方库来处理任务的持久性,例如TapePriority Job Queue。@maral - corsair992
请注意,这里重要的部分是持久化_task_(可以使用Service来完成),而不是它的状态或结果,否则问题就不适用于本问题。@maral,如果您能提供一种查询操作状态的方法,那么在Fragment重新实例化时确定是否需要关闭DialogFragment将足以解决问题。如果您使用我提到的任务排队库,它们应该天然地提供此功能。 - corsair992

-1
  • 您的问题很长,没有任何代码示例,我会尽力给出最好的答案。我为您制作了一个项目,演示了如何在Android中使用onSaveinstance状态,这将解决所有可能的问题。

  • 我写了一篇解释性文章,可以消除您所有的疑虑。


在Android生命周期中处理片段

  • Activitycontainer来放置fragments

  • Activityoncreate中挂载第一个fragment

  • 当我们使用片段添加替换等操作时,需要使用android fragment引用而不是classreference

  • 每次将片段挂载到容器中时,都要将片段保留在backstack

  • 在项目中,我们可以观察到我们正在收集所有小部件的状态,并在onpause()中将其存储在本地变量中,并使用此本地变量,然后将此本地变量传递到onSaveInstance()事件中访问这些标记在onActivityCreatd()中设置为视图对象。我们使用此过程,因为局部变量保存在类中,但视图对象在onSaveInstance中为null。这个特定的观察结果非常重要,观察到了Activity-FragmentOne-FragmentTwo-FragmentOne-OrientationChange-OrientationChange

  • Android static objects能够保留状态onOrientation change,但dynamic Objects必须使用上述的本地变量重置onOrientation更改的值

  • 如果我们使用动态片段,则必须在使用saveInstanceState恢复动态对象之前,在OnActivityCreated()事件中创建动态对象

  • 片段在onPause事件中添加到后退栈中

  • 如果屏幕导航是Activity-FragmentOne-FragmentTwo-FragmentOne-FragmentTwo,则按下返回按钮时导航将作为FragmentTwo-FragmentOne-FragmentTwo-FragmentOne-Activity工作,因此我们可以清楚地观察到我们的路径由android后退栈跟踪

  • 如果路径是Activity-FragmentOne并且第一次更改方向,则触发的事件如下 MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • 如果路径是Activity-FragmentOne-orientationchange并且第一次更改方向,则触发的事件如下

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • 如果路径是 Activity-FragmentOne-屏幕旋转-屏幕旋转,并且第一次更改方向,则触发的事件如下:

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • 如果路径是Activity-FragmentOne-orientationchange-orientationchange-FragmentTwo并且第一次更改方向,则触发的事件如下:

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onStop- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentTwo-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • 如果路径是Activity-FragmentOne-orientationchange-orientationchange-FragmentTwo-orientationchange,并且第一次更改方向,则触发的事件如下:

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onStop- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentTwo-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentTwo-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume


总结::

  • 现在记住,onPause事件总是在关闭片段之前触发,存储您想要保留的值onOrientation,当phonecall来电时,任何其他情况

  • 接下来,将执行onSaveInstanceState,因此使用这些本地变量在此处设置数据以设置bundle中的数据

  • 接下来,活动将被推送到backstack,单击主页按钮时不会丢失数据

  • 因此,当onActivityCreatedbundle获取数据并将其存储回您的views时,请记住

  • 还要记住,Android在销毁片段时始终完成完整的生命周期,如果将其发送到backstack,则片段不会被销毁(当您按主页按钮时),只有当它需要更多空间时才会自动销毁。


如果您需要更多信息,请告诉我


1
这个回答并没有解决我的问题。我不是在处理方向改变、使用 onSaveInstanceState() 或者活动生命周期的问题。请看最后一段:如果 AsyncTask 在活动处于后台时完成,我想知道如何确保 FragmentActivity 的状态(UI)会被更新(或者在用户下次导航到它时将被更新)。 - maral
@ maral .... 好的....我想我无法分析你的问题....但是我提到如何处理事件的方式仍然是正确的!......场景1:onPostExecute中导航到下一个活动,以便在完成AsyncTask事务之前不会发生导航.....场景2:如果您正在处理数据库,则可以将其添加到Transaction中.....在其中一个答案中,kcoppock也描述了一种正确处理此情况的方法。!.....希望这有所帮助 - Devrath

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接