将 LoaderManager.LoaderCallbacks<Cursor> 与自定义 RecyclerView.Adapter 一起使用时出现问题
Problem using LoaderManager.LoaderCallbacks<Cursor> with custom RecyclerView.Adapter
我尝试使用 ListView 以及 LoaderManager.LoaderCallbacks 和自定义 CursorAdapter 加载列表,它工作正常。但我正在尝试使用 RecyclerView 和自定义 RecyclerView.Adapter 来完成相同的任务,但我遇到了这个问题:
我是第一次显示列表,但是当我旋转设备时列表消失了。
这是代码,请看一下
目录活动
public class CatalogActivity extends AppCompatActivity implements ItemAdapter.OnItemClickListener,
LoaderManager.LoaderCallbacks<Cursor> {
private static final int ITEMS_LOADER_ID = 1;
public static final String EXTRA_ITEM_NAME = "extra_item_name";
public static final String EXTRA_ITEM_STOCK = "extra_item_stock";
@BindView(R.id.list_items)
RecyclerView mListItems;
private ItemAdapter mItemAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_catalog);
ButterKnife.bind(this);
setupListItems();
getLoaderManager().initLoader(ITEMS_LOADER_ID, null, this);
}
private void setupListItems() {
mListItems.setHasFixedSize(true);
LayoutManager layoutManager = new LinearLayoutManager(this);
mListItems.setLayoutManager(layoutManager);
mListItems.setItemAnimator(new DefaultItemAnimator());
mListItems.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
mItemAdapter = new ItemAdapter(getApplicationContext(), this);
mListItems.setAdapter(mItemAdapter);
}
@Override
public void OnClickItem(int position) {
Intent intent = new Intent(this, EditorActivity.class);
Item item = mItemAdapter.getItems().get(position);
intent.putExtra(EXTRA_ITEM_NAME, item.getName());
intent.putExtra(EXTRA_ITEM_STOCK, item.getStock());
startActivity(intent);
}
private ArrayList<Item> getItems(Cursor cursor) {
ArrayList<Item> items = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
int columnIndexName = cursor.getColumnIndex(ItemEntry.COLUMN_NAME);
int columnIndexStock = cursor.getColumnIndex(ItemEntry.COLUMN_STOCK);
int id = cursor.getInt(columnIndexId);
String name = cursor.getString(columnIndexName);
int stock = Integer.parseInt(cursor.getString(columnIndexStock));
items.add(new Item(id, name, stock));
}
}
return items;
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
switch (loaderId) {
case ITEMS_LOADER_ID: {
String[] projection = {
ItemEntry._ID,
ItemEntry.COLUMN_NAME,
ItemEntry.COLUMN_STOCK
};
return new CursorLoader(
this,
ItemEntry.CONTENT_URI,
projection,
null,
null,
null
);
}
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mItemAdapter.setItems(getItems(cursor));
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}
项目适配器
public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
private ArrayList<Item> mItems;
private OnItemClickListener mOnItemClickListener;
private Context mContext;
public ItemAdapter(Context context, OnItemClickListener onItemClickListener) {
mOnItemClickListener = onItemClickListener;
mContext = context;
}
public void setItems(ArrayList<Item> items) {
if (items != null) {
mItems = items;
notifyDataSetChanged();
}
}
public ArrayList<Item> getItems() {
return mItems;
}
public interface OnItemClickListener {
void OnClickItem(int position);
}
public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
@BindView(R.id.tv_item)
TextView tv_item;
@BindView(R.id.tv_stock)
TextView tv_stock;
public ItemViewHolder(@NonNull View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
int position = getAdapterPosition();
mOnItemClickListener.OnClickItem(position);
}
}
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_inventory, parent, false);
return new ItemViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder itemViewHolder, int position) {
final Item item = mItems.get(position);
itemViewHolder.tv_item.setText(item.getName());
itemViewHolder.tv_stock.setText(mContext.getString(R.string.display_stock, item.getStock()));
}
@Override
public int getItemCount() {
if (mItems == null) {
return 0;
} else {
return mItems.size();
}
}
}
我无法找出确切的问题。请帮忙。
而不是 LoaderManager.initLoader()
调用 LoaderManager.restartLoader()
简而言之,这里的问题是,在轮换之后,您得到的 Cursor
与您之前在轮换之前循环过的相同,但您没有考虑其当前位置。
A Cursor
跟踪并维护自己在其记录集中的位置,我相信您已经从它包含的各种 move*()
方法中了解到这一点。第一次创建时,Cursor
的位置会设置在第一条记录之前;即,其位置将设置为 -1
.
当您第一次启动您的应用程序时,LoaderManager
调用 onCreateLoader()
,您的 CursorLoader
在其中被实例化,然后导致它加载并交付其 Cursor
, Cursor
的位置在 -1
。此时,while (cursor.moveToNext())
循环按预期工作,因为第一个 moveToNext()
调用会将其移动到第一个位置(索引 0
),然后移动到之后的每个可用位置,直到最后。
然而,在旋转时,LoaderManager
确定它已经具有请求的 Loader
(由 ID 确定),它本身看到它已经具有适当的 Cursor
加载,所以它会立即再次传送相同的 Cursor
对象。 (这是 Loader
框架的一个主要特点——它不会重新加载它已经拥有的资源,无论配置如何变化。)这就是问题的症结所在。那Cursor
一直留在旋转前最后移动到的位置;即,在它的尽头。因此,Cursor
不能 moveToNext()
,因此 while
循环根本就不会运行,在初始
onLoadFinished()
,旋转前。
根据给定的设置,最简单的修复方法是您自己手动重新定位 Cursor
。例如,在 getItems()
中,如果 Cursor
不为空,则将 if
更改为 moveToFirst()
,并将 while
更改为 do-while
,所以我们不会无意中跳过第一条记录。即:
if (cursor != null && cursor.moveToFirst()) {
do {
int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
...
} while (cursor.moveToNext());
}
有了这个,当相同的 Cursor
对象被重新传送时,它的位置有点 "reset" 到位置 0
。由于该位置直接在第一条记录上,而不是在它之前(记住,最初是 -1
),我们更改为 do-while
,这样第一个 moveToNext()
调用就不会跳过Cursor
.
中的第一条记录
备注:
我要提到的是,可以实现 RecyclerView.Adapter
来直接获取 Cursor
,类似于旧的 CursorAdapter
。在这种情况下,Cursor
必须在 onBindViewHolder()
方法中移动到每个项目的正确位置,并且不需要单独的 ArrayList
。虽然有点费力,但翻译CursorAdapter
to a RecyclerView.Adapter
isn't terribly difficult. Alternatively, there are certainly solutions already available. (For example, possibly, this one,虽然我不能保证,atm,我经常看到一个值得信赖的用户经常推荐它。)
我还要提到原生 Loader
框架已经 deprecated, in favor of the newer ViewModel
/LiveData
architecture framework in support libraries。然而,最新的 androidx 库似乎有自己的内部改进的 Loader
框架,它是上述 ViewModel
/LiveData
设置的简单包装器。这似乎是利用已知 Loader
结构的一种很好、简单的方法,同时仍然受益于最近的架构改进。
我尝试使用 ListView 以及 LoaderManager.LoaderCallbacks 和自定义 CursorAdapter 加载列表,它工作正常。但我正在尝试使用 RecyclerView 和自定义 RecyclerView.Adapter 来完成相同的任务,但我遇到了这个问题:
我是第一次显示列表,但是当我旋转设备时列表消失了。
这是代码,请看一下
目录活动
public class CatalogActivity extends AppCompatActivity implements ItemAdapter.OnItemClickListener,
LoaderManager.LoaderCallbacks<Cursor> {
private static final int ITEMS_LOADER_ID = 1;
public static final String EXTRA_ITEM_NAME = "extra_item_name";
public static final String EXTRA_ITEM_STOCK = "extra_item_stock";
@BindView(R.id.list_items)
RecyclerView mListItems;
private ItemAdapter mItemAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_catalog);
ButterKnife.bind(this);
setupListItems();
getLoaderManager().initLoader(ITEMS_LOADER_ID, null, this);
}
private void setupListItems() {
mListItems.setHasFixedSize(true);
LayoutManager layoutManager = new LinearLayoutManager(this);
mListItems.setLayoutManager(layoutManager);
mListItems.setItemAnimator(new DefaultItemAnimator());
mListItems.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
mItemAdapter = new ItemAdapter(getApplicationContext(), this);
mListItems.setAdapter(mItemAdapter);
}
@Override
public void OnClickItem(int position) {
Intent intent = new Intent(this, EditorActivity.class);
Item item = mItemAdapter.getItems().get(position);
intent.putExtra(EXTRA_ITEM_NAME, item.getName());
intent.putExtra(EXTRA_ITEM_STOCK, item.getStock());
startActivity(intent);
}
private ArrayList<Item> getItems(Cursor cursor) {
ArrayList<Item> items = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
int columnIndexName = cursor.getColumnIndex(ItemEntry.COLUMN_NAME);
int columnIndexStock = cursor.getColumnIndex(ItemEntry.COLUMN_STOCK);
int id = cursor.getInt(columnIndexId);
String name = cursor.getString(columnIndexName);
int stock = Integer.parseInt(cursor.getString(columnIndexStock));
items.add(new Item(id, name, stock));
}
}
return items;
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
switch (loaderId) {
case ITEMS_LOADER_ID: {
String[] projection = {
ItemEntry._ID,
ItemEntry.COLUMN_NAME,
ItemEntry.COLUMN_STOCK
};
return new CursorLoader(
this,
ItemEntry.CONTENT_URI,
projection,
null,
null,
null
);
}
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mItemAdapter.setItems(getItems(cursor));
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}
项目适配器
public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
private ArrayList<Item> mItems;
private OnItemClickListener mOnItemClickListener;
private Context mContext;
public ItemAdapter(Context context, OnItemClickListener onItemClickListener) {
mOnItemClickListener = onItemClickListener;
mContext = context;
}
public void setItems(ArrayList<Item> items) {
if (items != null) {
mItems = items;
notifyDataSetChanged();
}
}
public ArrayList<Item> getItems() {
return mItems;
}
public interface OnItemClickListener {
void OnClickItem(int position);
}
public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
@BindView(R.id.tv_item)
TextView tv_item;
@BindView(R.id.tv_stock)
TextView tv_stock;
public ItemViewHolder(@NonNull View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
int position = getAdapterPosition();
mOnItemClickListener.OnClickItem(position);
}
}
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_inventory, parent, false);
return new ItemViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder itemViewHolder, int position) {
final Item item = mItems.get(position);
itemViewHolder.tv_item.setText(item.getName());
itemViewHolder.tv_stock.setText(mContext.getString(R.string.display_stock, item.getStock()));
}
@Override
public int getItemCount() {
if (mItems == null) {
return 0;
} else {
return mItems.size();
}
}
}
我无法找出确切的问题。请帮忙。
而不是 LoaderManager.initLoader()
调用 LoaderManager.restartLoader()
简而言之,这里的问题是,在轮换之后,您得到的 Cursor
与您之前在轮换之前循环过的相同,但您没有考虑其当前位置。
A Cursor
跟踪并维护自己在其记录集中的位置,我相信您已经从它包含的各种 move*()
方法中了解到这一点。第一次创建时,Cursor
的位置会设置在第一条记录之前;即,其位置将设置为 -1
.
当您第一次启动您的应用程序时,LoaderManager
调用 onCreateLoader()
,您的 CursorLoader
在其中被实例化,然后导致它加载并交付其 Cursor
, Cursor
的位置在 -1
。此时,while (cursor.moveToNext())
循环按预期工作,因为第一个 moveToNext()
调用会将其移动到第一个位置(索引 0
),然后移动到之后的每个可用位置,直到最后。
然而,在旋转时,LoaderManager
确定它已经具有请求的 Loader
(由 ID 确定),它本身看到它已经具有适当的 Cursor
加载,所以它会立即再次传送相同的 Cursor
对象。 (这是 Loader
框架的一个主要特点——它不会重新加载它已经拥有的资源,无论配置如何变化。)这就是问题的症结所在。那Cursor
一直留在旋转前最后移动到的位置;即,在它的尽头。因此,Cursor
不能 moveToNext()
,因此 while
循环根本就不会运行,在初始
onLoadFinished()
,旋转前。
根据给定的设置,最简单的修复方法是您自己手动重新定位 Cursor
。例如,在 getItems()
中,如果 Cursor
不为空,则将 if
更改为 moveToFirst()
,并将 while
更改为 do-while
,所以我们不会无意中跳过第一条记录。即:
if (cursor != null && cursor.moveToFirst()) {
do {
int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
...
} while (cursor.moveToNext());
}
有了这个,当相同的 Cursor
对象被重新传送时,它的位置有点 "reset" 到位置 0
。由于该位置直接在第一条记录上,而不是在它之前(记住,最初是 -1
),我们更改为 do-while
,这样第一个 moveToNext()
调用就不会跳过Cursor
.
备注:
我要提到的是,可以实现
RecyclerView.Adapter
来直接获取Cursor
,类似于旧的CursorAdapter
。在这种情况下,Cursor
必须在onBindViewHolder()
方法中移动到每个项目的正确位置,并且不需要单独的ArrayList
。虽然有点费力,但翻译CursorAdapter
to aRecyclerView.Adapter
isn't terribly difficult. Alternatively, there are certainly solutions already available. (For example, possibly, this one,虽然我不能保证,atm,我经常看到一个值得信赖的用户经常推荐它。)我还要提到原生
Loader
框架已经 deprecated, in favor of the newerViewModel
/LiveData
architecture framework in support libraries。然而,最新的 androidx 库似乎有自己的内部改进的Loader
框架,它是上述ViewModel
/LiveData
设置的简单包装器。这似乎是利用已知Loader
结构的一种很好、简单的方法,同时仍然受益于最近的架构改进。