情况
我有一个带有 AutoCompleteTextView 的 Activity 。在您键入时,AutoCompleteTextView 会从您的联系人中查找匹配的姓名并将其显示在列表中。如果在显示此列表时更改设备方向,则 Activity 将崩溃(源代码后提供错误消息)。
笔记
我正在为 ICS 4.0.3 开发并在 Nexus S 设备上进行测试。我正在尝试遵循使用 LoaderManager 生成和管理游标的最佳实践。我的理解是 LoaderManager 应该在方向更改 ( http://developer.android.com/guide/topics/fundamentals/loaders.html#callback ) 中保留光标数据,但情况似乎并非如此。
因为 CursorAdapter 要我在过滤约束太小而无法使用时返回原始的、未过滤的游标,所以我是:
问题似乎是在方向更改后调用了 onLoadFinished LoaderManager 回调,但它传递的游标(原始游标?)在重新定位期间已关闭。
如果我将 Activity 配置为通过将以下内容添加到 list 中的
activity
声明来管理方向更改本身:保存的原始光标应在方向更改时保留(对吗?)。虽然应用程序没有崩溃,但会出现另一个相关问题:
在这种情况下,我的原始光标似乎也消失了。我猜应用程序没有崩溃,因为当我的 Activity 配置为自行管理方向更改时,没有调用 onLoadFinished 回调
我的问题
源代码
查看 -
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>
Activity -
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
警告消息 - 当 Activity 配置为自行管理方向更改时
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)
最佳答案
我发现我的问题的解决方案(解决方案)分为两部分:
注意:对于任何关注的人,我在运行此代码时仍然会遇到一些错误,但这不是致命的,而且它似乎与游标无关,所以我不在这里解决它。
在 CursorAdapter 中生成游标
最大的复杂之处在于 CursorAdapter 中的
runQueryOnBackgroundThread
方法要求返回一个游标。使用 LoaderManager 时,在异步回调之前您不会接触游标,这有缺点:runQueryOnBackgroundThread
方法中生成和检索游标。 runQueryOnBackgroundThread
是第一个使用新过滤约束调用的方法。 runQueryOnBackgroundThread
方法将其光标交给 changeCursor
方法,该方法关闭已更改的光标(在使用 LoaderManager/CursorLoader 时我们不会这样做),因此无论如何我们都不想遵循该工作流程。 默认情况下,CursorAdapter 的
runQueryOnBackgroundThread
方法只调用 CursorAdapter 的 FilterQueryProvider 的 runQuery
方法(如果已定义)。出于以下几个原因,我选择定义 FilterQueryProvider 而不是覆盖 runQueryOnBackgroundThread
方法:注意:
runQuery
方法仍然需要返回一个游标,所以我们不能回避这个问题。我决定在我的 FilterQueryProvider 的
runQuery
方法中生成一个虚拟游标。然后,由于该虚拟游标将移交给 CursorAdapter 的 changeCursor
方法,因此我覆盖了 changeCursor
以简单地关闭它传递的每个游标。runQuery
方法还启动包含过滤约束的异步 LoaderManager 调用。然后 LoaderManager 回调负责交换生成的新游标。注意:理想情况下,我想您可以覆盖调用
runQueryOnBackgroundThread
的函数,并让它进行异步 LoaderManager 调用,但我不知道那是什么。卡在光标上
我试图区分未过滤的游标和过滤的游标,以便在过滤约束为空时可以使用未过滤的游标。在无数次阅读 Android 3.0 - what are the advantages of using LoaderManager instances exactly? 之后,我意识到接受的答案是使用相同的 CursorLoader 来生成所有游标。
我没有尝试保留原始的未过滤游标,而是决定在需要时生成一个新的未过滤游标。
onCreateLoader
LoaderManager 回调变得有点复杂(但更像我看到的示例),而 onLoadFinished
回调变得更加简单(就像我看到的示例)。源代码
Activity - 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();
}
}
关于java - 使用 AutoCompleteTextView 和 CursorAdapter 时,更改设备方向会导致 Activity 崩溃,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/9742017/