使用AutoCompleteTextView和CursorAdapter时,更改设备方向会导致活动崩溃

时间:2012-03-16 17:42:23

标签: java android

情况

我有一个带有AutoCompleteTextView的Activity。在您键入时,AutoCompleteTextView会从您的联系人中查找匹配的名称,并将其显示在列表中。如果在显示此列表时更改了设备方向,则活动崩溃(源代码后提供错误消息)。

备注

我正在为ICS 4.0.3开发并在Nexus S设备上进行测试。我试图遵循使用LoaderManager生成和管理游标的最佳实践。我的理解是LoaderManager应该在方向更改(http://developer.android.com/guide/topics/fundamentals/loaders.html#callback)之间保留游标数据,但似乎并非如此。

因为当过滤约束太小而无法使用时,CursorAdapter要我返回原始未过滤的游标,我是:

  1. 将游标保存为活动的静态变量,以便在不符合过滤条件时使用(对不当的术语表示抱歉。我是Java新手)。
  2. 防止CursorAdapter替换游标后关闭游标,除非它通过将游标与保存的游标进行比较来确认它不是原始游标。
  3. 问题似乎是在方向更改后调用了onLoadFinished LoaderManager回调,但是在重新定向期间它正在传递的光标(原始光标?)被关闭。

    如果我通过在清单中的activity声明中添加以下内容来配置我的活动以管理方向更改:

      

    机器人:configChanges = “取向|屏幕尺寸”

    保存的原始光标应该在方向更改中保留(对吗?)。虽然应用程序没有崩溃,但还会出现另一个相关问题:

    • 如果我输入几个字母,更改设备方向,然后开始删除字母,一旦我收到1或0个字母,LogCat会给我一个警告,表示我试图在光标关闭后访问它。

    在这种情况下,我的原始光标似乎也消失了。我猜测应用程序没有崩溃,因为当我的活动配置为自行管理方向更改时,不会调用onLoadFinished回调

    我的问题

    1. 我正确地假设我的光标在设备方向改变时被销毁了吗?
    2. 如何在设备方向更改时保留光标和/或其数据?
    3. 源代码

      查看 - home.xml

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical" >
      
          <AutoCompleteTextView
              android:id="@+id/newPlayer_edit"
              android:inputType="text"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_gravity="center_horizontal"
              android:hint="Contact"
              android:singleLine="true" >
              <requestFocus />
          </AutoCompleteTextView>
      
      </LinearLayout>
      

      活动 - Home.java

      public class Home extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
      
      // Constants
      private static final String TAG = "HOME";
      private static final boolean DEBUG = true;
      public static final int LOADER_CONTACTS_CURSOR = 1;
      
      // Variables
      private AdapterContacts adapter;
      public static Cursor originalCursor = null;
      
      
      /**
       * Overrides
       */
      
      @Override
      public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
      
          // Set the view
          setContentView(R.layout.home);
      
          // Initialize CursorAdapter
          adapter = new AdapterContacts(this, null, 0);
      
          // Attach CursorAdapter to AutoCompleteTextView field
          AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
          field.setAdapter(adapter);
      
          // Initialize Cursor using LoaderManager
          LoaderManager.enableDebugLogging(true);
          getLoaderManager().initLoader(LOADER_CONTACTS_CURSOR, null, this);
      }
      
      @Override
      public void onDestroy() {
          if (DEBUG) Log.i(TAG, "Destroying activity");
          super.onDestroy();
      }
      
      @Override
      public Loader<Cursor> onCreateLoader(int id, Bundle args) {
          if (DEBUG) Log.i(TAG, "Loader Callback: creating loader");
          return new CursorLoader(this, ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
      }
      
      @Override
      public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
          if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
          // If no cursor has been loaded before, reserve this cursor as the original
          // It will be returned by the CursorAdapter when the filter constraint is null 
          if (originalCursor == null) {
              originalCursor = cursor;
          }
      
          // add the cursor to the adapter
          adapter.swapCursor(cursor);
      }
      
      @Override
      public void onLoaderReset(Loader<Cursor> loader) {
          if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
          adapter.swapCursor(null);
      }
      }
      

      CursorAdapter - AdapterContacts.java

      public class AdapterContacts extends CursorAdapter {
      
      // Constants
      private static final String TAG = "AdapterContacts";
      private static final boolean DEBUG = true;
      
      // Variables
      private TextView mName;
      private ContentResolver mContent;
      
      /**
       * Constructor
       */
      public AdapterContacts(Context context, Cursor c, int flags) {
          super(context, c, flags);
          mContent = context.getContentResolver();
      }
      
      /**
       * Overrides
       */
      
      @Override
      public View newView(Context context, Cursor cursor, ViewGroup parent) {
          // Inflate the views that create each row of the dropdown list
          final LayoutInflater inflater = LayoutInflater.from(context);
          final LinearLayout ret = new LinearLayout(context);
          ret.setOrientation(LinearLayout.VERTICAL);
      
          mName = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
          ret.addView(mName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
      
          /*
          int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
          mName.setText(cursor.getString(nameIdx));
          */
      
          return ret;
      }
      
      @Override
      public void bindView(View view, Context context, Cursor cursor) {
          // Fill the dropdown row with data from the cursor
          int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
          String name = cursor.getString(nameIdx);
          ((TextView) ((LinearLayout) view).getChildAt(0)).setText(name);
      }
      
      @Override
      public String convertToString(Cursor cursor) {
          // Convert the dropdown list entry that the user clicked on
          // into a string that will fill the AutoCompleteTextView
          int nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
          return cursor.getString(nameCol);
      }
      
      @Override
      public void changeCursor(Cursor newCursor) {
          // Because a LoaderManager is used to initialize the originalCursor
          // changeCursor (which closes cursors be default when they're released)
          // is overridden to use swapCursor (which doesn't close cursors). 
          Cursor oldCursor = swapCursor(newCursor);
      
          // Any swapped out cursors that are not the original cursor must 
          // then be closed manually.
          if (oldCursor != Home.originalCursor) {
              oldCursor.close();
          }
      }
      
      @Override
      public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
          // If their is a constraint, generate and return a new cursor
          if (constraint != null) {
              // I'd love to use a LoaderManager here too,
              // but haven't quite figured out the best way.
              if (DEBUG) Log.i(TAG, "Constraint is not null: " + constraint.toString());
              Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, constraint.toString());
              return mContent.query(uri, null, null, null, null);
          }
      
          // If no constraint, return the originalCursor
          if (DEBUG) Log.i(TAG, "Constraint is null");
          return Home.originalCursor;
      }
      }
      

      错误消息

      03-16 10:39:34.839: E/AndroidRuntime(22097): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.myapp.basic/com.myapp.basic.Home}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1956)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1981)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3351)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.access$700(ActivityThread.java:123)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1151)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Handler.dispatchMessage(Handler.java:99)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Looper.loop(Looper.java:137)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.main(ActivityThread.java:4424)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invokeNative(Native Method)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invoke(Method.java:511)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at dalvik.system.NativeStart.main(Native Method)
      03-16 10:39:34.839: E/AndroidRuntime(22097): Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.getColumnNames(BulkCursorToCursorAdaptor.java:170)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndex(AbstractCursor.java:248)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndexOrThrow(AbstractCursor.java:265)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.CursorWrapper.getColumnIndexOrThrow(CursorWrapper.java:78)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.widget.CursorAdapter.swapCursor(CursorAdapter.java:338)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:70)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:1)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:438)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.finishRetain(LoaderManager.java:309)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl.finishRetain(LoaderManager.java:765)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.Activity.performStart(Activity.java:4485)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1929)
      03-16 10:39:34.839: E/AndroidRuntime(22097):    ... 12 more
      

      警告消息 - 将活动配置为自行管理方向更改时

      03-16 10:47:50.804: W/Filter(22739): An exception occured during performFiltering()!
      03-16 10:47:50.804: W/Filter(22739): android.database.StaleDataException: Attempted to access a cursor after it has been closed.
      03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
      03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.getCount(BulkCursorToCursorAdaptor.java:81)
      03-16 10:47:50.804: W/Filter(22739):    at android.database.CursorWrapper.getCount(CursorWrapper.java:57)
      03-16 10:47:50.804: W/Filter(22739):    at android.widget.CursorFilter.performFiltering(CursorFilter.java:53)
      03-16 10:47:50.804: W/Filter(22739):    at android.widget.Filter$RequestHandler.handleMessage(Filter.java:234)
      03-16 10:47:50.804: W/Filter(22739):    at android.os.Handler.dispatchMessage(Handler.java:99)
      03-16 10:47:50.804: W/Filter(22739):    at android.os.Looper.loop(Looper.java:137)
      03-16 10:47:50.804: W/Filter(22739):    at android.os.HandlerThread.run(HandlerThread.java:60)
      

2 个答案:

答案 0 :(得分:6)

我发现问题的解决方案(解决方案)分为两部分:

  1. 我无法在CursorAdapter中手动继续生成游标。我不得不开始使用装载程序管理器
  2. 我不需要挂在任何游标上。
  3. 注意:对于任何关注的人,我在运行此代码时仍然会遇到一些错误,但这并不是致命的,它似乎与游标无关,所以我没有解决它这里。

    在CursorAdapter

    中生成游标

    最大的复杂因素是CursorAdapter中的runQueryOnBackgroundThread方法要求返回游标。当使用LoaderManager时,在异步回调之前你不会触及游标,这有缺点:

    1. 您无法从runQueryOnBackgroundThread方法中生成和检索光标。
    2. 您无法提前生成游标,因为runQueryOnBackgroundThread是使用新过滤约束调用的第一个方法。
    3. runQueryOnBackgroundThread方法将光标移到changeCursor方法,该方法关闭更改的游标(使用LoaderManager / CursorLoader时我们不做的事情),所以我们不要无论如何都不想遵循这个工作流程。
    4. 默认情况下,CursorAdapter的runQueryOnBackgroundThread方法只调用CursorAdapter的FilterQueryProvider的runQuery方法(如果已定义)。我选择定义FilterQueryProvider而不是覆盖runQueryOnBackgroundThread方法,原因如下:

      • 可以从实例化CursorAdapter的Activity中定义FilterQueryProvider,并且使用来自Activity的LoaderManager更容易在CursorAdapter中使用它。
      • 如果可能的话,我更愿意让代码按照它的意图行事。

      注意:runQuery方法仍然需要返回游标,因此我们不能解决这个问题。

      我决定在我的FilterQueryProvider的runQuery方法中生成一个虚拟光标。然后,由于该虚拟光标将被移交给CursorAdapter的changeCursor方法,我会覆盖changeCursor以简单地关闭它传递的每个光标。

      runQuery方法还会启动包含过滤约束的异步LoaderManager调用。然后,LoaderManager回调负责交换生成的新游标。

      注意:理想情况下,我认为您可以覆盖调用runQueryOnBackgroundThread的函数,并让它调用异步LoaderManager,但我无法弄清楚它是什么。 < / p>

      挂在游标上

      我试图区分未过滤和过滤的游标,当过滤约束为空时,可以使用未过滤的游标。在无条件阅读Android 3.0 - what are the advantages of using LoaderManager instances exactly?之后,我意识到接受的答案是使用相同的CursorLoader来生成所有游标。

      我没有试图挂在原始的未过滤光标上,而是决定在需要时我只生成一个新的未过滤光标。 onCreateLoader LoaderManager回调变得有点复杂(但更像我见过的例子),onLoadFinished回调变得更加简单(就像我见过的例子)。

      源代码

      活动 - home.java

      public class Home extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
      
      // Constants
      private static final String TAG = "Home";
      private static final boolean DEBUG = true;
      public static final int LOADER_CONTACTS_CURSOR = 1;
      
      // Variables
      private AdapterContacts adapter;
      
      
      
      /**
       * Overrides
       */
      
      @Override
      public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
      
          // Set the view
          setContentView(R.layout.home);
      
          // Initialize CursorAdapter
          adapter = new AdapterContacts(this, null, 0);
          final LoaderManager.LoaderCallbacks<Cursor> iFace = this;
          adapter.setFilterQueryProvider(new FilterQueryProvider() {
              public Cursor runQuery(CharSequence constraint) {
                  if (constraint != null) {
                      Bundle bundle = new Bundle();
                      bundle.putCharSequence("constraint", constraint);
                      getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, bundle, iFace);
                  } else {
                      getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, null, iFace);
                  }
                  return getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
              }
          });
      
          // Attach CursorAdapter to AutoCompleteTextView field
          AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
          field.setAdapter(adapter);
      
          // Initialize Cursor using LoaderManagers
          LoaderManager.enableDebugLogging(true);
          getLoaderManager().initLoader(LOADER_CONTACTS_CURSOR, null, this);
      }
      
      @Override
      public void onDestroy() {
          if (DEBUG) Log.i(TAG, "Destroying activity");
          super.onDestroy();
      }
      
      @Override
      public Loader<Cursor> onCreateLoader(int id, Bundle args) {
          if (DEBUG) Log.i(TAG, "Loader Callback: creating loader");
          Uri baseUri;
      
          if (args != null) {
              CharSequence constraint = args.getCharSequence("constraint");
              if (DEBUG) Log.i(TAG, "Constraint: " + constraint.toString());
              baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(constraint.toString()));
          } else {
              if (DEBUG) Log.i(TAG, "No Constraint");
              baseUri = ContactsContract.Contacts.CONTENT_URI;
          }
          return new CursorLoader(this, baseUri, null, null, null, null);
      }
      
      @Override
      public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
          if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
          adapter.swapCursor(cursor);
      }
      
      @Override
      public void onLoaderReset(Loader<Cursor> loader) {
          if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
          adapter.swapCursor(null);
      }
      }
      

      CursorAdapter - AdapterContacts.java

      public class AdapterContacts extends CursorAdapter {
      
      // Constants
      private static final String TAG = "AdapterContacts";
      private static final boolean DEBUG = true;
      
      // Variables
      private TextView mName;
      
      /**
       * Constructor
       */
      public AdapterContacts(Context context, Cursor c, int flags) {
          super(context, c, flags);
      }
      
      /**
       * Overrides
       */
      
      @Override
      public View newView(Context context, Cursor cursor, ViewGroup parent) {
          // Inflate the views that create each row of the dropdown list
          final LayoutInflater inflater = LayoutInflater.from(context);
          final LinearLayout ret = new LinearLayout(context);
          ret.setOrientation(LinearLayout.VERTICAL);
      
          mName = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
          ret.addView(mName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
      
          /*
          int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
          mName.setText(cursor.getString(nameIdx));
          */
      
          return ret;
      }
      
      @Override
      public void bindView(View view, Context context, Cursor cursor) {
          // Fill the dropdown row with data from the cursor
          int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
          String name = cursor.getString(nameIdx);
          ((TextView) ((LinearLayout) view).getChildAt(0)).setText(name);
      }
      
      @Override
      public String convertToString(Cursor cursor) {
          // Convert the dropdown list entry that the user clicked on
          // into a string that will fill the AutoCompleteTextView
          int nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
          return cursor.getString(nameCol);
      }
      
      @Override
      public void changeCursor(Cursor newCursor) {
          newCursor.close();
      }
      }
      

答案 1 :(得分:0)

如果您使用Loader来管理Adapter的{​​{1}},那么您需要不惜一切代价避免使用Cursor。由于适配器中的adapter.filter()需要返回filter()这是不可能的,因为加载器是异步的并且在后台线程中运行。

如何用加载器替换Cursor

  1. 在onSelect / Item / TextInput侦听器中,将选定/输入值存储在类变量中。

  2. 将所有对adapter.filter()的来电替换为myAdapter.filter()

  3. myLoadManager.restartLoader(...)方法中,使用您现在拥有的存储的选定/输入值,因为步骤1,动态生成您的sql / cursor查询并运行它。

  4. 这就是事件顺序的演绎方式:

    1. 用户选择一个微调器下拉列表并选择“USA”。

    2. 您将“USA”存储在类变量中,然后调用onCreateLoader()

    3. 加载管理器会销毁之前的加载管理器,并创建一个名为myLoadManager.restartLoader(...)的新加载管理器,其中包含您自动生成的查询代码。

    4. 新加载程序运行更新的查询并通知您的适配器刷新&amp;重绘它的数据。

    5. 为什么要使用onCreateLoader()

      1. 基于慢速数据的非阻塞UI更新的免费/更轻松的异步管理。比Loaders容易得多。

      2. 游标的可用内存管理。在重新查询或活动结束时自动清除/关闭AsyncTasks