当用户旋转手机并将所有数据保留在 ArrayAdapter 中时,保留列表视图项的良好解决方案

Posted

技术标签:

【中文标题】当用户旋转手机并将所有数据保留在 ArrayAdapter 中时,保留列表视图项的良好解决方案【英文标题】:Good solution to retain listview items when user rotate phone and keep all data in ArrayAdapter 【发布时间】:2013-05-17 13:12:21 【问题描述】:

我正在使用带有 listView 的 Fragment。我通过自定义加载程序(来自互联网)中接收的数据填充与此列表视图关联的 ArrayAdapter。自定义 ArrayAdapter 支持无限滚动(分页)。

当用户旋转设备并在 ListView 中保持滚动位置时,在 ArrayAdapter 中存储项目的最佳方式是什么?

我正在考虑使用 ArrayAdapter 创建非可视 Fragment,并使用 setRetainInstance 方法保存值。

对更好的解决方案有什么建议吗?

【问题讨论】:

developer.android.com/guide/topics/resources/… 你可以检查这个片段androiddesignpatterns.com/2013/04/…你可以检查onConfigurationChanged @developer.android.com/reference/android/app/Fragment.html 如果你将 ArrayAdapter 设为静态,在 onCreate 中检查它是否为 null,如果它创建它,则在它上面调用 invalidate(强制重绘)? 【参考方案1】:

要使用 Android 框架和 Fragment 生命周期,您应该在 Fragment 中实现 onSaveInstanceState 方法。为简单起见,我假设您有一个可以访问的 String 值数组(我通常扩展 ArrayAdapter 以封装视图构造并提供访问整个底层数据集的便捷方法):

public void onSaveInstanceState(Bundle savedState) 

    super.onSaveInstanceState(savedState);

    // Note: getValues() is a method in your ArrayAdapter subclass
    String[] values = mAdapter.getValues(); 
    savedState.putStringArray("myKey", values);


然后您可以在 onCreate 方法(或 onCreateView 或 onActivityCreated - 请参阅 the Fragment JavaDoc)中检索数据,如下所示:

public void onCreate (Bundle savedInstanceState) 

    super.onCreate(savedInstanceState);

    if (savedInstanceState != null) 
        String[] values = savedInstanceState.getStringArray("myKey");
        if (values != null) 
           mAdapter = new MyAdapter(values);
        
    

    ...


这可确保正确处理所有生命周期事件,而不会丢失数据,包括设备旋转和用户切换到其他应用程序。不使用onSaveInstanceState而使用内存的危险是Android回收该内存的危险。保存状态不会受此影响,但使用实例变量或隐藏片段会导致数据丢失。

如果savedStateInstance 为空,则没有要恢复的状态。

if (values != null) 只是为了防止没有保存数组的可能性,但是如果您编写 ArrayAdapter 来处理空数据集,则不需要它。

如果您的行是您自己的一个类而不是单个数据项的实例,最终的解决方案是在该类上实现 Parcelable 接口,那么您可以使用savedState.putParcelableArray("myKey", myArray)。你会惊讶于知道如何实现 Parcelable 是多么有用——它允许你在内部意图中传递你的类并允许你编写更简洁的代码。

【讨论】:

保存数据听起来不错,但如何在 ListView 中保持滚动位置 嗯 ListView 有一个 onSaveInstanceState() 方法返回一个 Parcelable,和一个 onRestoreInstanceState(Parcelable state) 方法来恢复状态。如果我没记错的话,保存的状态包括滚动位置。 要保留滚动位置,还要在onSaveInstanceState()中保存:listView.getFirstVisiblePosition()。将适配器设置为 listView 后,只需添加以下行: listView.setSelectionFromTop( index , 0 );其中 index 是您保存的索引。 @vickey:很好的评论,正是我想要的。 如果我们不使用数组适配器,而是使用 baseAdapter,而您将 Object 的数组列表传递给适配器构造函数怎么办?【参考方案2】:

当设备旋转时,应用会重新启动。所以onSaveInstance 在应用程序被销毁之前被调用。您可以将数组适配器保存在onSaveInstance 中,当再次启动应用时最终调用onCreate 时,您可以检索数组适配器并将其设置为列表视图。

【讨论】:

是的,只有前台活动被重新创建,但同时活动内部的片段也被重新创建。 “保存一个 ArrayAdaptor”没有意义。正确的做法是保存 ArrayAdaptor 使用的数据,然后重新创建 ArrayAdaptor - 请参阅我的大纲解决方案。 明白了。您不保存数组适配器,您只保存列表数据,无论它是数组还是数组列表或对象的数组列表。【参考方案3】:

将适配器的项目保存在onSaveInstance 中非常容易。

现在您需要保存位置(滚动)。你可以这样做

简单的方法

由于更改 screenOrientation 无论如何都会搞砸,您可以允许一些边际错误,并简单地在您的onSaveInstance 中使用listView.getFirstVisiblePosition() 保存第一个可见项目。

然后在您的onRestoreInstance 中,您可以检索此索引以使用listview.setSelection(position) 滚动到同一项目。当然,你可以使用listview.smoothScrollToPosition(position),但这会很奇怪。


艰难的道路

要获得更准确的位置,您需要再下降 1 级:获取滚动位置(不是索引)并保存该位置。然后在恢复后,滚动回这个位置。在您的onSaveInstance 中,保存从listview.getScrollY() 返回的位置。当您在onRestoreInstance 中检索此位置时,您可以使用listview.scrollTo(0, position) 滚动回此位置。


为什么真的很难?

简单。您很可能无法滚动回该位置,因为您的列表视图尚未通过测量和布局。您可以通过在使用getViewObserver().addOnGlobalLayoutListener 设置适配器后等待布局来克服这个问题。在此回调中,发布一个 runnable 以滚动到该位置。


我建议使用第一种“简单方法”,因为通常当屏幕方向发生变化时,用户可能会将手指向后移动。我们只需要向他展示“给予或接受”相同的物品。

【讨论】:

【参考方案4】:

当方向改变活动生命周期调用onPause 然后onRestart 做这样的事情 -

@Override
protected void onRestart() 
    super.onRestart();
    mAdapter.getFilter().filter(getFilterSettings());
    mAdapter.notifyDataSetChanged();

【讨论】:

你错了,文档清楚地表明活动是在方向改变时重新创建的。 只需创建方法 onPause()onRestart() 并在其中打印日志,看看会发生什么。 做到了。没有 configChanges 参数的正常活动不会调用 onRestart。 挂钩到 onSaveInstanceState。这正是它的用途。【参考方案5】:

为列表视图保存数据以免在片​​段重新创建期间一次又一次地重新加载,就是将该数据保存到类的静态成员中。 类似的东西

class YourData
public static List<YourDataObject> listViewData;

然后从适配器首先从互联网或本地数据库加载数据 然后将检索到的数据设置为该静态成员,如下所示:

 @Override
    public void onCreate(@Nullable Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
       //Run the process to get data from database
      YourData.listViewData = dataRetrievedFromDatabase;
     

然后在您的 onResume 方法中将这些值更新到适配器,例如:

@Override
    public void onResume() 
        super.onResume();
       if(YourData.listViewData!=null)
          yourListAdapater.setData = YourData.listViewData;
          listView.setAdapter(yourListAdapater);
        else
      //run your method to get data from server 
         
    

这可用于为单个会话保存任何类型的数据

【讨论】:

这不会可靠地工作,因为当应用程序或活动不活动时,Android 可以回收内存。要使用 Android 框架和 Fragment 生命周期,您应该在 Fragment 中实现 onSaveInstanceState 方法。【参考方案6】:

我不知道支持它的 ArrayAdapterList 的内容,但是如何使支持列表的数据可序列化、保存,然后在重新创建视图时加载它?

传统上,当您尝试在应用程序关闭或因留在后台而有被杀死的风险时尝试存储数据时,我已经看到了这种方法。这里有一个很棒的post on serialization,并附有示例代码。他们遍历了自定义对象的ArrayList,将其写入文件并稍后重新打开。我认为如果您实施这种方法,在活动被销毁之前将支持ListArrayAdapter 的数据写入onPause(),那么您可以在重新创建活动时重新加载该文件。

其他方法(a/k/a 备用计划):

(1) 简单但草率 - 如果您的列表是一些原始的,例如字符串列表,您总是可以考虑将值单独写入SharedPreferences 并在重新加载时回收它们。只要确保在存储过程中分配一些唯一的 id。

注意虽然这可能有效,但SharedPreferecnes 通常不适合处理大量数据,因此如果您的列表很长,我会避免使用这种方法。但是,对于一些数据点,我认为这不是问题。

(2) 稍微困难但有风险 - 如果您支持适配器的 List 包含实现 parcelable 或已经可序列化的对象,请考虑通过 IntentList 传递给现有的活动,该活动是在后台并在重新创建活动时使用回调来检索该数据。这类似于您创建背景Fragment 的想法。这两种方法都存在您所针对的ActivityFragment 不存在的风险。

【讨论】:

当你有 onSaveInstanceState 方法时为什么要使用 SharedPreferences ?至于将数据传递给应用程序的另一部分 - 如果 Android 在后台销毁应用程序以恢复内存,这将是混乱的,最终无济于事。了解 Activity 和 Fragment 生命周期并使用它。 是的,我认为这不是一个好主意,当然 savedInstanceState 更好。但是,如果您只需要在短时间内存储几个字符串,这只是一种快速的方法。

以上是关于当用户旋转手机并将所有数据保留在 ArrayAdapter 中时,保留列表视图项的良好解决方案的主要内容,如果未能解决你的问题,请参考以下文章

使用 viewmodel 旋转设备时如何保留我的 edittext 数据?

用户杀死Android应用后保存状态

Android:在方向更改时保留相机预览,而其他视图可以旋转

如何在触摸时旋转图像并将图像拖动到另一个图像上

iPad:在屏幕上保留选定的表格视图单元格

如何为后退按钮保留 Web 表单内容