Async Task blocking UI thread - android

UPDATE
Thanks to #EmanuelSeibold I was able to pinpoint the problem to the update of the recyclerview. The AsyncTask works in the background just fine, and only the adapter update of the recyclerview freezes the UI.
UPDATE2
I found it was indeed my layout setup. I forgot to remove the nestedScrollView around the RecyclerView. That seemed to cause a rendering conflict.
I dug my way through answers here and blog posts, but just don't seem to be able to find a solution.
I am fairly new to Android development and trying to get an idea on multi-threading.
The scenario: I have an app that holds a SQLite database with course data and implemented a search function that queries that database. This blocks the UI-thread for roughly ~3 seconds.
I therefore implemented an AsyncTask to keep the UI responsive, but my UI is still blocked while the search is ongoing.
Thanks in advance!
Here the code:
Search activity
public class Activity_Search extends Activity_Base {
private RecyclerView rv;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
setActionBarTitle(R.string.title_search);
findViewById(R.id.searchCourseTitle).setOnKeyListener(new View.OnKeyListener() {
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_ENTER:
startSearch();
break;
default:
return false;
}
return true;
}
});
rv = (RecyclerView) findViewById(R.id.searchRecycler);
Adapter_Search adapter = new Adapter_Search(this, null);
rv.setAdapter(adapter);
rv.setLayoutManager(new LinearLayoutManager(this));
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.search_FAB);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
startSearch();
}
});
}
private void startSearch() {
findViewById(R.id.searchCourseTitle).clearFocus();
if (this.getCurrentFocus() != null) {
InputMethodManager imm =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.getCurrentFocus().getWindowToken(), 0);
}
{getting input}
String[] columns = {...};
String selection = {formatting input};
Async_Search search = new Async_Search(this);
search.execute(columns, new String[]{selection});
}
public void onSearchCompleted(Cursor results) {
((Adapter_Search) rv.getAdapter()).changeCursor(results);
}
}
AsyncTask
public class Async_Search extends AsyncTask<String[], Void, Cursor> {
private Activity activity;
public Async_Search (Activity activity) {
this.activity = activity;
}
#Override
protected Cursor doInBackground(String[]... params) {
SQLiteDatabase db = SQL_Database.getInstance(activity).getWritableDatabase();
String[] columns = params[0];
String selection = params[1][0];
return db.query(...)
}
#Override
protected void onPostExecute(Cursor results) {
((Activity_Search) activity).onSearchCompleted(results);
}
}
Recycler adapter
public class Adapter_Search extends RecyclerCursorAdapter<Adapter_Search.ViewHolder>{
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView name, ects, studipCode, termYear, lecturers, fields;
public ViewHolder (View view) {
super(view);
{id lookups}
}
}
public Adapter_Search(Context context, Cursor cursor) {
super(context, cursor);
}
#Override
public ViewHolder onCreateViewHolder (ViewGroup parent, int ViewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.search_list_entry, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
#Override
public void onBindViewHolder (ViewHolder viewHolder, Cursor cursor) {
TextView name, ects, studipCode, termYear, lecturers, fields;
name = viewHolder.name;
ects = viewHolder.ects;
studipCode = viewHolder.studipCode;
termYear = viewHolder.termYear;
lecturers = viewHolder.lecturers;
fields = viewHolder.fields;
String termandyear = cursor.getString(cursor.getColumnIndexOrThrow(SQL_Database.COURSE_COLUMN_TERM)) +
String.format("%.2s",cursor.getString(cursor.getColumnIndexOrThrow(SQL_Database.COURSE_COLUMN_YEAR)));
name.setText(cursor.getString(cursor.getColumnIndexOrThrow(SQL_Database.COURSE_COLUMN_COURSE)));
String credits = cursor.getString(cursor.getColumnIndexOrThrow(SQL_Database.COURSE_COLUMN_ECTS)) + " ECTS";
ects.setText(credits);
{and so on}
}
}
Base adapter class
public abstract class RecyclerCursorAdapter <ViewHolder extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<ViewHolder> {
private Context mContext;
private Cursor mCursor;
private boolean mDataValid;
private int mRowIdColumn;
private DataSetObserver mDataSetObserver;
public RecyclerCursorAdapter(Context context, Cursor cursor) {
mContext = context;
mCursor = cursor;
mDataValid = cursor != null;
mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;
mDataSetObserver = new NotifyingDataSetObserver();
if (mCursor != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
}
public Cursor getCursor() {
return mCursor;
}
#Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
}
return 0;
}
#Override
public long getItemId(int position) {
if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIdColumn);
}
return 0;
}
#Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
public abstract void onBindViewHolder(ViewHolder viewHolder, Cursor cursor);
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
if (!mDataValid) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
onBindViewHolder(viewHolder, mCursor);
}
/**
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
* closed.
*/
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
/**
* Swap in a new Cursor, returning the old Cursor. Unlike
* {#link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
* closed.
*/
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
final Cursor oldCursor = mCursor;
if (oldCursor != null && mDataSetObserver != null) {
oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (mCursor != null) {
if (mDataSetObserver != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
notifyDataSetChanged();
} else {
mRowIdColumn = -1;
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
return oldCursor;
}
private class NotifyingDataSetObserver extends DataSetObserver {
#Override
public void onChanged() {
super.onChanged();
mDataValid = true;
notifyDataSetChanged();
}
#Override
public void onInvalidated() {
super.onInvalidated();
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
}
}

Maybe try to remove these lines from the Async Search.
private Activity activity;
public Async_Search (Activity activity) {
this.activity = activity;
}
Instead call like this:
Async_Search search = new Async_Search();
search.execute(columns, new String[]{selection});
I don't know if this will work, but my Asynctask doesn't use the activity stuff yours has.

Related

Items clickable but invisible on screen: downloaded via AsyncTaskLoader

I'm storing a list of the most frequented transit lines in a Content Provider and a RecyclerView Adapter to be available in online and offline viewing. They are downloaded into an activity via AsyncTaskLoader. They are accessible from the menu option in the activity below. I debugged and it shows that items are being added to the Content Provider in another activity. Then, when I click on "most frequented" in the menu option, the screen is blank but the items are clickable(both on and offline). I've tried to debug but it's not showing any errors.
I found a similar thread:
RecyclerView items are clickable but invisible
In my case, all the fonts and colors are correct. I know that the Loader is deprecated as of sdk 28. However, it's not even working in 27. Thank you in advance.
Activity where the data is added to the Content Provider:
public class StationListActivity extends AppCompatActivity implements StationsAdapter.StationsAdapterOnClickHandler, TubeStationAsyncTaskInterface,
LoaderManager.LoaderCallbacks<Cursor>
{
//Tag for the log messages
private static final String TAG = StationListActivity.class.getSimpleName();
#BindView(R.id.recyclerview_station)
RecyclerView mStationRecyclerView;
private StationsAdapter stationsAdapter;
private ArrayList<Stations> stationsArrayList = new ArrayList<>();
private static final String KEY_STATIONS_LIST = "stations_list";
private static final String KEY_LINE_NAME = "line_name";
Lines lines;
public String lineId;
private Context context;
private TextView lineNameStation;
private String lineNameToString;
#BindView(R.id.favorites_button)
Button favoritesButton;
/**
* Identifier for the favorites data loader
*/
private static final int FAVORITES_LOADER = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_station_list);
context = getApplicationContext();
// Bind the views
ButterKnife.bind(this);
stationsAdapter = new StationsAdapter(this, stationsArrayList, context);
mStationRecyclerView.setAdapter(stationsAdapter);
RecyclerView.LayoutManager mStationLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mStationRecyclerView.setLayoutManager(mStationLayoutManager);
lineNameStation = (TextView) findViewById(R.id.line_name_station);
//add to favorites
favoritesButton.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View view)
{
ContentValues values = new ContentValues();
values.put(TubeLineContract.TubeLineEntry.COLUMN_LINES_ID, lines.getLineId());
values.put(TubeLineContract.TubeLineEntry.COLUMN_LINES_NAME, lines.getLineName());
Uri uri = getContentResolver().insert(TubeLineContract.TubeLineEntry.CONTENT_URI, values);
if (uri != null)
{
Toast.makeText(getBaseContext(), uri.toString(), Toast.LENGTH_LONG).show();
Toast.makeText(StationListActivity.this, R.string.favorites_added, Toast.LENGTH_SHORT).show();
favoritesButton.setVisibility(View.GONE);
}
}
});
/*
* Starting the asyncTask so that stations load when the activity opens.
*/
if (getIntent() != null && getIntent().getExtras() != null)
{
if (savedInstanceState == null)
{
lines = getIntent().getExtras().getParcelable("Lines");
lineId = lines.getLineId();
TubeStationAsyncTask myStationTask = new TubeStationAsyncTask(this);
myStationTask.execute(lineId);
lineNameStation.setText(lines.getLineName());
} else
{
stationsArrayList = savedInstanceState.getParcelableArrayList(KEY_STATIONS_LIST);
stationsAdapter.setStationsList(stationsArrayList);
}
}
// Kick off the loader
getLoaderManager().initLoader(FAVORITES_LOADER, null, this);
}
#Override
public void returnStationData(ArrayList<Stations> simpleJsonStationData) {
if (null != simpleJsonStationData) {
stationsAdapter = new StationsAdapter(this, simpleJsonStationData, StationListActivity.this);
stationsArrayList = simpleJsonStationData;
mStationRecyclerView.setAdapter(stationsAdapter);
stationsAdapter.setStationsList(stationsArrayList);
}
}
#Override
public void onClick(Stations stations) {
Intent intent = new Intent(StationListActivity.this, StationScheduleActivity.class);
intent.putExtra("Stations", stations);
intent.putExtra("Lines", lines);
startActivity(intent);
}
#Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle)
{
String[] projection = {TubeLineContract.TubeLineEntry._ID, TubeLineContract.TubeLineEntry.COLUMN_LINES_ID,};
String[] selectionArgs = new String[]{lineId};
switch (loaderId)
{
case FAVORITES_LOADER:
return new CursorLoader(this, // Parent activity context
TubeLineContract.TubeLineEntry.CONTENT_URI, // Provider content URI to query
projection, // Columns to include in the resulting Cursor
TubeLineContract.TubeLineEntry.COLUMN_LINES_ID + "=?",
selectionArgs,
null); // Default sort order
default:
throw new RuntimeException("Loader Not Implemented: " + loaderId);
}
}
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor)
{
if ((cursor != null) && (cursor.getCount() > 0))
{
//"Add to Favorites" button is disabled in the StationList Activity when the user clicks on a line stored in Favorites
favoritesButton.setEnabled(false);
}
}
public void onLoaderReset(Loader<Cursor> cursorLoader)
{
}
#Override
protected void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(KEY_STATIONS_LIST, stationsArrayList);
super.onSaveInstanceState(outState);
}
}
Activity with the menu option:
public class MainActivity extends AppCompatActivity implements LinesAdapter.LinesAdapterOnClickHandler, TubeLineAsyncTaskInterface,
LoaderManager.LoaderCallbacks<Cursor> {
// Tag for logging
private static final String TAG = MainActivity.class.getSimpleName();
#BindView(R.id.recyclerview_main)
RecyclerView mLineRecyclerView;
private LinesAdapter linesAdapter;
private ArrayList<Lines> linesArrayList = new ArrayList<>();
private Context context;
private static final String KEY_LINES_LIST = "lines_list";
CoordinatorLayout mCoordinatorLayout;
#BindView(R.id.pb_loading_indicator)
ProgressBar mLoadingIndicator;
private AdView adView;
private FavoritesAdapter favoritesAdapter;
private static final int FAVORITES_LOADER_ID = 0;
private int mPosition = RecyclerView.NO_POSITION;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context = getApplicationContext();
// Bind the views
ButterKnife.bind(this);
mCoordinatorLayout = findViewById(R.id.coordinatorLayout);
favoritesAdapter = new FavoritesAdapter(this, context);
linesAdapter = new LinesAdapter(this, linesArrayList, context);
mLineRecyclerView.setAdapter(linesAdapter);
RecyclerView.LayoutManager mLineLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mLineRecyclerView.setLayoutManager(mLineLayoutManager);
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof LinesAdapter.LinesAdapterViewHolder) return 0;
return super.getSwipeDirs(recyclerView, viewHolder);
}
// Called when a user swipes left or right on a ViewHolder
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
// Here is where you'll implement swipe to delete
//Construct the URI for the item to delete
//[Hint] Use getTag (from the adapter code) to get the id of the swiped item
// Retrieve the id of the task to delete
int id = (int) viewHolder.itemView.getTag();
// Build appropriate uri with String row id appended
String stringId = Integer.toString(id);
Uri uri = TubeLineContract.TubeLineEntry.CONTENT_URI;
uri = uri.buildUpon().appendPath(stringId).build();
// TODO (2) Delete a single row of data using a ContentResolver
int rowsDeleted = getContentResolver().delete(uri, null, null);
Log.v("CatalogActivity", rowsDeleted + " rows deleted from the movie database");
// TODO (3) Restart the loader to re-query for all tasks after a deletion
getSupportLoaderManager().restartLoader(FAVORITES_LOADER_ID, null, MainActivity.this);
}
}).attachToRecyclerView(mLineRecyclerView);
/*
* Starting the asyncTask so that lines load upon launching the app.
*/
if (savedInstanceState == null)
{
if (isNetworkStatusAvailable(this))
{
TubeLineAsyncTask myLineTask = new TubeLineAsyncTask(this);
myLineTask.execute(NetworkUtils.buildLineUrl());
} else {
Snackbar
.make(mCoordinatorLayout, "Please check your internet connection", Snackbar.LENGTH_INDEFINITE)
.setAction("Retry", new MyClickListener())
.show();
}
} else {
linesArrayList = savedInstanceState.getParcelableArrayList(KEY_LINES_LIST);
linesAdapter.setLinesList(linesArrayList);
}
getSupportLoaderManager().initLoader(FAVORITES_LOADER_ID, null, MainActivity.this);
mLineRecyclerView.setAdapter(favoritesAdapter);
}
public class MyClickListener implements View.OnClickListener {
#Override
public void onClick(View v) {
// Run the AsyncTask in response to the click
TubeLineAsyncTask myLineTask = new TubeLineAsyncTask(MainActivity.this);
myLineTask.execute();
}
}
#Override
public void returnLineData(ArrayList<Lines> simpleJsonLineData) {
mLoadingIndicator.setVisibility(View.INVISIBLE);
if (null != simpleJsonLineData) {
linesAdapter = new LinesAdapter(this, simpleJsonLineData, MainActivity.this);
linesArrayList = simpleJsonLineData;
mLineRecyclerView.setAdapter(linesAdapter);
linesAdapter.setLinesList(linesArrayList);
} else {
showErrorMessage();
}
}
#Override
public void onClick(Lines lines) {
Intent intent = new Intent(MainActivity.this, StationListActivity.class);
intent.putExtra("Lines", lines);
startActivity(intent);
}
//Display if there is no internet connection
public void showErrorMessage() {
Snackbar
.make(mCoordinatorLayout, "Please check your internet connection", Snackbar.LENGTH_INDEFINITE)
.setAction("Retry", new MyClickListener())
.show();
mLineRecyclerView.setVisibility(View.INVISIBLE);
mLoadingIndicator.setVisibility(View.VISIBLE);
}
public static boolean isNetworkStatusAvailable(Context context) {
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null &&
activeNetwork.isConnectedOrConnecting();
}
#Override
public Loader<Cursor> onCreateLoader(int id, final Bundle loaderArgs)
{
return new AsyncTaskLoader<Cursor>(this)
{
// Initialize a Cursor, this will hold all the task data
Cursor mFavoritesData = null;
// onStartLoading() is called when a loader first starts loading data
#Override
protected void onStartLoading()
{
if (mFavoritesData != null)
{
// Delivers any previously loaded data immediately
deliverResult(mFavoritesData);
}
else
{
// Force a new load
forceLoad();
}
}
// loadInBackground() performs asynchronous loading of data
#Override
public Cursor loadInBackground()
{
// Will implement to load data
// Query and load all task data in the background; sort by priority
// [Hint] use a try/catch block to catch any errors in loading data
try
{
return getContentResolver().query(TubeLineContract.TubeLineEntry.CONTENT_URI,
null,
null,
null,
TubeLineContract.TubeLineEntry.COLUMN_LINES_ID);
}
catch (Exception e)
{
Log.e(LOG_TAG, "Failed to asynchronously load data.");
e.printStackTrace();
return null;
}
}
// deliverResult sends the result of the load, a Cursor, to the registered listener
public void deliverResult(Cursor data)
{
mFavoritesData = data;
super.deliverResult(data);
}
};
}
/**
* Called when a previously created loader has finished its load.
*
* #param loader The Loader that has finished.
* #param data The data generated by the Loader.
*/
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data)
{
favoritesAdapter.swapCursor(data);
if (mPosition == RecyclerView.NO_POSITION) mPosition = 0;
mLineRecyclerView.smoothScrollToPosition(mPosition);
}
/**
* Called when a previously created loader is being reset, and thus
* making its data unavailable.
* onLoaderReset removes any references this activity had to the loader's data.
*
* #param loader The Loader that is being reset.
*/
#Override
public void onLoaderReset(Loader<Cursor> loader)
{
favoritesAdapter.swapCursor(null);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
/* Use AppCompatActivity's method getMenuInflater to get a handle on the menu inflater */
MenuInflater inflater = getMenuInflater();
/* Use the inflater's inflate method to inflate our menu layout to this menu */
inflater.inflate(R.menu.main, menu);
/* Return true so that the menu is displayed in the Toolbar */
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item)
{
TubeLineAsyncTask myLineTask = new TubeLineAsyncTask(this);
switch (item.getItemId())
{
case R.id.most_frequented_favorites:
getSupportLoaderManager().restartLoader(FAVORITES_LOADER_ID, null, MainActivity.this);
favoritesAdapter = new FavoritesAdapter(this, MainActivity.this);
mLineRecyclerView.setAdapter(favoritesAdapter);
return true;
case R.id.line_list:
myLineTask.execute();
return true;
default:
return super.onOptionsItemSelected(item);
}}
#Override
protected void onSaveInstanceState(Bundle outState) {
outState.putParcelableArrayList(KEY_LINES_LIST, linesArrayList);
super.onSaveInstanceState(outState);
}
}
RecyclerViewAdapter class:
public class FavoritesAdapter extends RecyclerView.Adapter<FavoritesAdapter.FavoritesAdapterViewHolder>
{
private static final String TAG = FavoritesAdapter.class.getSimpleName();
private Context context;
private Cursor cursor;
private LinesAdapter.LinesAdapterOnClickHandler mClickHandler;
public FavoritesAdapter(LinesAdapter.LinesAdapterOnClickHandler clickHandler, Context context)
{
mClickHandler = clickHandler;
this.context = context;
}
public class FavoritesAdapterViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
#BindView(R.id.line_name)
TextView lineName;
public FavoritesAdapterViewHolder(View view) {
super(view);
ButterKnife.bind(this, view);
view.setOnClickListener(this);
}
#Override
public void onClick(View v) {
cursor.moveToPosition(getAdapterPosition());
String lineName = cursor.getString(cursor.getColumnIndexOrThrow(TubeLineContract.TubeLineEntry.COLUMN_LINES_NAME));
String lineId = cursor.getString(cursor.getColumnIndexOrThrow(TubeLineContract.TubeLineEntry.COLUMN_LINES_ID));
Lines line = new Lines(lineName, lineId);
mClickHandler.onClick(line);
}
}
#Override
public void onBindViewHolder(FavoritesAdapter.FavoritesAdapterViewHolder holder, int position)
{
// get to the right location in the cursor
cursor.moveToPosition(position);
// Determine the values of the wanted data
int lineIdIndex = cursor.getColumnIndexOrThrow(TubeLineContract.TubeLineEntry.COLUMN_LINES_ID);
final int id = cursor.getInt(lineIdIndex);
holder.itemView.setTag(id);
}
#Override
public FavoritesAdapter.FavoritesAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType)
{
Context context = viewGroup.getContext();
int layoutIdForListItem = R.layout.line_list_item;
LayoutInflater inflater = LayoutInflater.from(context);
boolean shouldAttachToParentImmediately = false;
View view = inflater.inflate(layoutIdForListItem, viewGroup, shouldAttachToParentImmediately);
return new FavoritesAdapter.FavoritesAdapterViewHolder(view);
}
public Cursor swapCursor(Cursor c)
{
// check if this cursor is the same as the previous cursor (mCursor)
if (cursor == c)
{
return null; // bc nothing has changed
}
Cursor temp = cursor;
this.cursor = c; // new cursor value assigned
//check if this is a valid cursor, then update the cursor
if (c != null)
{
this.notifyDataSetChanged();
}
return temp;
}
#Override
public int getItemCount()
{
if (null == cursor)
return 0;
return cursor.getCount();
}
}

viewholder does not maintain its state when scrolled off screen

I have a music player app where when an item is clicked it changes its state to 'play' state where it gets expanded to show a seek bar and other changes also take place and vice versa.
The problem is whenever I scroll up or down, the view holder swaps its current state(playing, paused visibility in this case) with some other random view in the list. It is also worth mentioning that this is consistent throughout the list so after every certain number of view holders, the state is the same as the one that was clicked (so if view at position 0 is in play visibility state after every 10 views a view is in the same state, note however the song still plays for the right one).
Here is the Adapter code (extends from a base adapter and the methods for play/pause visibility can be found here which are used in the song fragment code below):
public class AllSongsAdapter extends BaseSongAdapter<AllSongsAdapter.AllSongsItemHolder> {
private ArrayList<Song> songList;
public AllSongsAdapter(){
super(null);
}
//OnCreateViewHolder was called for every view;
//FIX: return 0 for same type of views.
#Override
public int getItemViewType(int position) {
return 0;
}
//cursor moves to the appropriate position in the list so we just have to update our views
#Override
public void onBindViewHolder(AllSongsItemHolder holder, Cursor cursor) {
if(cursor!=null) {
int i = cursor.getPosition();
Log.d("on Bind", "i:" + i);
Song songItem = songList.get(i);
holder.songItemName.setText(songItem.title);
holder.songItemArtistName.setText(songItem.artistName);
holder.songItemAlbumName.setText(songItem.albumName);
}
}
#Override
public void swapCursor(Cursor newCursor) {
super.swapCursor(newCursor);
songList = SongsLoader.getSongsForCursor(newCursor);
}
#Override
public AllSongsItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.d("CREATE VIEW HOLDER", "holder" );
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.songs_item_list, parent, false);
return new AllSongsItemHolder(v);
}
private Uri getAlbumArtUri(long albumId) {
return ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId);
}
public void setPlayVisibility(View child, RecyclerView rv) {
//View v = rv.getChildAt(rv.getChildAdapterPosition(child));
AllSongsItemHolder holder = (AllSongsItemHolder) rv.getChildViewHolder(child);
// if(getItemId(position) == rv.getChildItemId(v)){}
holder.seekBar.setVisibility(View.VISIBLE);
holder.songItemTimer.setVisibility(View.VISIBLE);
holder.songItemImage.setScaleType(ImageView.ScaleType.CENTER);
holder.songItemImage.setBackgroundColor(Color.parseColor("#303F9F"));
}
public void setPauseVisibility(View child, RecyclerView rv) {
//View v = rv.getChildAt(rv.getChildAdapterPosition(child));
AllSongsItemHolder holder = (AllSongsItemHolder) rv.getChildViewHolder(child);
//if(getItemId(position) == rv.getChildItemId(v)){}
holder.seekBar.setVisibility(View.GONE);
holder.songItemTimer.setVisibility(View.GONE);
holder.songItemImage.setScaleType(ImageView.ScaleType.FIT_XY);
holder.songItemImage.setBackgroundColor(0);
}
static class AllSongsItemHolder extends RecyclerView.ViewHolder {
private ImageView songItemImage, songItemOptionDropDown;
private TextView songItemName, songItemAlbumName, songItemArtistName;
private View separator;
private SeekBar seekBar;
private ImageView nowPlayingIcon;
private TextView songItemTimer;
public AllSongsItemHolder(View v) {
super(v);
songItemImage = v.findViewById(R.id.songItemImage);
songItemOptionDropDown = v.findViewById(R.id.songItemOptionDropDown);
songItemAlbumName = v.findViewById(R.id.songItemAlbumName);
songItemArtistName = v.findViewById(R.id.songItemArtistName);
songItemName = v.findViewById(R.id.songItemName);
separator = v.findViewById(R.id.separator);
seekBar = v.findViewById(R.id.seekbar);
songItemTimer = v.findViewById(R.id.songItemTimer);
// nowPlayingIcon = v.findViewById(R.id.nowPlayingIcon);
}
}
}
I noticed that when getItemViewType returns position this problem does not occur because recycler view holds an instance for every item in the list. But clearly this is not an adequate solution because it slows down the scrolling when first loaded as it has to create every view. Could be something to do with this?
BaseAdapter code:
public abstract class BaseSongAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
protected Cursor mCursor;
protected int mRowIDColumn;
protected boolean mDataValid;
public BaseSongAdapter(Cursor c) {
init(c);
}
public BaseSongAdapter() {}
void init(Cursor c) {
boolean cursorPresent = c != null;
mCursor = c;
mDataValid = cursorPresent;
mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
setHasStableIds(true);
if(mDataValid && mCursor!=null) {
Log.d("VALID", "CURSOR");
}
}
#Override
public void onBindViewHolder(VH holder, int position) {
//Log.d("ON BIND","CALLED");
if (!mDataValid) {
throw new IllegalStateException("Cannot bind viewholder when cursor is in invalid state.");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("Could not move cursor to position " + position + " when trying to bind viewholder");
}
onBindViewHolder(holder, mCursor);
}
public abstract void onBindViewHolder(VH holder, Cursor cursor);
#Override
public int getItemViewType(int position) {
return position;
}
#Override
public int getItemCount() {
if (mDataValid) {
return mCursor.getCount();
} else {
return 0;
}
}
#Override
public long getItemId(int position) {
if (!mDataValid) {
throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state.");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("Could not move cursor to position " + position + " when trying to get an item id");
}
return mCursor.getLong(mRowIDColumn);
}
public void swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return;
}
//Log.d("TAG", "swapCursor");
//Cursor oldCursor = mCursor;
if (newCursor != null) {
mCursor = newCursor;
mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
// notify the observers about the new cursor
notifyDataSetChanged();
} else {
mCursor = null;
mRowIDColumn = -1;
mDataValid = false;
// notify the observers about the lack of a data set
notifyItemRangeRemoved(0, getItemCount());
}
//return oldCursor;
}
}
Song Fragment code where clicks are handled(in onActivityCreated):
public class AllSongsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private final int LOADER_ID_ALLSONGS = 0;
private Context ctx;
private AllSongsAdapter mAdapter;
private MediaPlayerHolder mediaPlayerHolder;
//on click play,pause variables
private boolean playingCLicked = false;
private boolean firstTime = true;
private String playbackState;
private View lastChildView;
private long currentSongId;
private long newID;
private long nextID; //id for next song
private int lastTrackPosition; //for auto next track
private RecyclerView rv;
public AllSongsFragment() {
super();
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
ctx = context;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(LOADER_ID_ALLSONGS, null, this);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.all_songs_fragment, container, false);
return view;
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
rv = (RecyclerView) getActivity().findViewById(R.id.fragmentList);
mediaPlayerHolder = new MediaPlayerHolder(getActivity());
mediaPlayerHolder.setPlaybackInfoListener(new PlaybackListener());
rv.addOnItemTouchListener(new RecyclerTouchListener(getActivity(), rv, new ClickListener() {
#Override
public void click(View view, final int position) {
Toast.makeText(getActivity(), "onClick " + position, Toast.LENGTH_SHORT).show();
newID = mAdapter.getItemId(position);
if (playingCLicked) {
if (mediaPlayerHolder.isPlaying()) {
if (currentSongId == newID) {
mAdapter.setPauseVisibility(view, rv);
lastChildView = view;
}
//PAUSE IT
mediaPlayerHolder.pause();
Log.d("playingclicked", "true" + "state" + playbackState);
//when a different song is clicked while current song is playing
if (playbackState.equalsIgnoreCase("paused") && newID != currentSongId) {
currentSongId = newID;
mAdapter.setPauseVisibility(lastChildView, rv);
mAdapter.setPlayVisibility(view, rv);
mediaPlayerHolder.reset();
mediaPlayerHolder.loadMedia(currentSongId);
mediaPlayerHolder.play();
lastTrackPosition = position;
lastChildView = view;
Log.d("else 1 positions", "currentsongID" + currentSongId + "lasttrack" + lastTrackPosition);
}
playingCLicked = !playingCLicked;
} else { //media is not playing
Log.d("else 1", "play");
if (firstTime) {
Log.d("different", "no");
currentSongId = newID;
lastTrackPosition = position;
lastChildView = view;
mediaPlayerHolder.reset();
mediaPlayerHolder.loadMedia(currentSongId);
Log.d("SelectedTrack", "" + mediaPlayerHolder.getMediaPlayer().getSelectedTrack(MEDIA_TRACK_TYPE_AUDIO));
firstTime = false;
}
if (newID != currentSongId) {
Log.d("different", "yes");
//mediaPlayerHolder.stop();
currentSongId = newID;
mediaPlayerHolder.reset();
mediaPlayerHolder.loadMedia(currentSongId);
firstTime = true;
}
if (currentSongId == newID)
mAdapter.setPlayVisibility(view, rv);
//PLAY IT
mediaPlayerHolder.play();
playingCLicked = !playingCLicked;
}
} else //----------playingclicked = false, first called--------------
{
if (!mediaPlayerHolder.isPlaying()) {
lastChildView = view;
mAdapter.setPlayVisibility(view, rv);
if (firstTime) {
Log.d("different", "no, first time");
currentSongId = newID;
lastTrackPosition = position;
lastChildView = view;
mediaPlayerHolder.reset();
mediaPlayerHolder.loadMedia(currentSongId);
Log.d("SelectedTrack", "" + mediaPlayerHolder.getMediaPlayer().getSelectedTrack(MEDIA_TRACK_TYPE_AUDIO));
firstTime = false;
}
Log.d("playbackState", playbackState + "currentId " + currentSongId);
//called when current song is paused and after playingClicked = true following is called if songId is different
if (newID != currentSongId) {
Log.d("different", "yes");
currentSongId = newID;
mediaPlayerHolder.stop();
mediaPlayerHolder.reset();
mediaPlayerHolder.loadMedia(currentSongId);
}
//PLAY IT
mediaPlayerHolder.play();
playingCLicked = !playingCLicked;
} else { //media is playing
Log.d("else 2", "pause");
if (newID == currentSongId) {
lastChildView = view;
lastTrackPosition = position;
mAdapter.setPauseVisibility(view, rv);
}
//PAUSE IT
mediaPlayerHolder.pause();
firstTime = false;
//when a different song is clicked while current song is playing
if (playbackState.equalsIgnoreCase("paused") && newID != currentSongId) {
currentSongId = newID;
mAdapter.setPauseVisibility(lastChildView, rv);
mAdapter.setPlayVisibility(view, rv);
mediaPlayerHolder.reset();
mediaPlayerHolder.loadMedia(currentSongId);
mediaPlayerHolder.play();
lastTrackPosition = position;
lastChildView = view;
Log.d("else 2 positions", "currentsongID" + currentSongId + "lasttrack" + lastTrackPosition);
}
playingCLicked = !playingCLicked;
}
}
}
#Override
public void onLongClick(View view, int position) {
Toast.makeText(getActivity(),"onLongClick " + position,Toast.LENGTH_SHORT).show();
}
}));
mAdapter = new AllSongsAdapter();
rv.setAdapter(mAdapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
rv.setLayoutManager(layoutManager);
//------------Temporary divider-----------------
RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(getActivity(), layoutManager.getOrientation());
rv.addItemDecoration(itemDecoration);
}
#Override
public Loader onCreateLoader(int id, Bundle args) {
Log.d("CREATE LOADER", "SUCCESS");
Uri musicUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
final String[] PROJECTION = {"_id", "title", "artist", "album", "duration", "track", "artist_id", "album_id"};
return new CursorLoader(ctx,
musicUri,
PROJECTION,
null,
null,
MediaStore.Audio.Media.DEFAULT_SORT_ORDER );
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.swapCursor(data);
Log.d("LOAD FINISHED", "SUCCESS");
}
#Override
public void onLoaderReset(Loader loader) {
Log.d("LOADER RESET", "CALLED");
mAdapter.swapCursor(null);
}
class PlaybackListener extends PlaybackStateListener{
#Override
void onDurationChanged(int duration) {
super.onDurationChanged(duration);
}
#Override
void onPositionChanged(int position) {
super.onPositionChanged(position);
}
#Override
void onStateChanged(int state) {
playbackState = PlaybackListener.convertStateToString(state);
}
#Override
void onPlaybackCompleted() {
//super.onPlaybackCompleted();
//pause visibility after completed
mAdapter.setPauseVisibility(lastChildView, rv);
}
#Override
void onLogUpdated(String formattedMessage) {
super.onLogUpdated(formattedMessage);
}
}
//---------------RECYCLER VIEW TOUCH EVENTS LISTENER CLASS------------------------
class RecyclerTouchListener implements RecyclerView.OnItemTouchListener {
private GestureDetector gestureDetector;
private ClickListener mListener;
public RecyclerTouchListener(Context ctx, final RecyclerView recyclerView, ClickListener clickListener){
mListener = clickListener;
gestureDetector = new GestureDetector(ctx, new GestureDetector.OnGestureListener() {
#Override
public boolean onDown(MotionEvent e) {
return false;
}
#Override
public void onShowPress(MotionEvent e) {
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
// Log.d("GESTURE DETECTED", "ACTION UP" + e);
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
#Override
public void onLongPress(MotionEvent e) {
//Log.d("GESTURE DETECTED", "LONG PRESS");
if(rv.getScrollState() == SCROLL_STATE_IDLE && !(rv.getScrollState() == SCROLL_STATE_DRAGGING)) {
View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (child != null && mListener != null) {
mListener.onLongClick(child, recyclerView.getChildLayoutPosition(child));
}
}
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
}
#Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
//Log.d("Intercept Event", "INTERCEPTING \n" + e);
if(rv.getScrollState() == SCROLL_STATE_IDLE && !(rv.getScrollState() == SCROLL_STATE_DRAGGING)) {
View child = rv.findChildViewUnder(e.getX(), e.getY());
if (child != null && mListener != null && gestureDetector.onTouchEvent(e)) {
mListener.click(child, rv.getChildLayoutPosition(child));
}
}
//true if onTouchEvent is to be called, false if you want gesture detector to handle events
return false;
}
#Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
#Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}
}
Any help would be greatly appreciated!!
You need to be setting the play/pause/whatever visibility in onBindViewHolder or else it will be whatever the last setting was even though a new model object is bound to that view.

RecyclerView blocks UI when setting the adapter

I have a requirement in which I have a populated database with over 300k rows. I have successfully implemented a CursorAdapter based in this question, with a mix of the two most up voted answers HERE.
I have implemented an AsyncTask for background service to perform the query to the database which is very fast, doesn't take more than 2-3 seconds. My ProgressDialog from the AsyncTask is at times hard to detect.
My problem is, when the task is done and I retrieve the Cursor, when I set the Adapter to the RecyclerView, the process freezes my UI for a few seconds until the data is set. It also happens when I perform a search (new query, same procedure as getting all the rows but with fewer rows), and replace the Cursor to update the data.
Here is some relevant code:
AsyncTask
#Override
protected Void doInBackground(Void... Void) {
if(type==Constants.GET_ZIP_CODES)
cursor = db.getAllZipCodes();
else
cursor = db.searchZipCodes(text);
return null;
}
#Override
protected void onPostExecute(Void Void) {
setAdapter();
mProgressDialog.dismiss();
super.onPostExecute(Void);
}
Methods
private void setAdapter(){
if(myAdapter == null){
myAdapter = new MyAdapter(getActivity(), cursor);
search_rv.setAdapter(myAdapter);
} else
myAdapter.swapCursor(cursor);
}
Since it is a search I don't have much to do here besides notifyDataSetChanged() because all the data changes widely in every search.
Is this normal? Since a RecyclerView only renders the visible views, why does it freeze and takes so long to update since the Cursor is already ready from the AsyncTask?
EDIT
I have changed my Adapter to avoid using CursorAdapter as #cricket_007 pointed out having an Adapter within an Adapter is bad design.
This is my Adapter:
public class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.ViewHolder> {
private Context mContext;
private Cursor mCursor;
private boolean mDataValid;
private int mRowIdColumn;
private DataSetObserver mDataSetObserver;
public SearchListAdapter(Context context, Cursor c) {
mContext = context;
mCursor=c;
mDataValid = c != null;
mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;
mDataSetObserver = new NotifyingDataSetObserver();
if (mCursor != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView itemTV;
ViewHolder(View itemView) {
super(itemView);
itemTV = (TextView) itemView.findViewById(R.id.itemTV);
}
}
#Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
#Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
}
return 0;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// Passing the binding operation to cursor loader
mCursor.moveToPosition(position);
String town = mCursor.getString(mCursor.getColumnIndex(Constants.COLUMN_TOWN));
String zipcode = mCursor.getString(mCursor.getColumnIndex(Constants.COLUMN_ZIPCODE));
String zipcode_etx = mCursor.getString(mCursor.getColumnIndex(Constants.COLUMN_ZIPCODE_EXTENSION));
holder.itemTV.setText(zipcode+"-"+zipcode_etx+", "+town);
}
#Override
public SearchListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_search_list_item,parent,false);
// Passing the inflater job to the cursor-adapter
return new SearchListAdapter.ViewHolder(itemView);
}
public void swapCursor(Cursor cursor) {
Cursor old = changeCursor(cursor);
if (old != null) {
old.close();
}
}
private Cursor changeCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
final Cursor oldCursor = mCursor;
if (oldCursor != null && mDataSetObserver != null) {
oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (mCursor != null) {
if (mDataSetObserver != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
notifyDataSetChanged();
} else {
mRowIdColumn = -1;
mDataValid = false;
notifyDataSetChanged();
}
return oldCursor;
}
private class NotifyingDataSetObserver extends DataSetObserver {
#Override
public void onChanged() {
super.onChanged();
mDataValid = true;
notifyDataSetChanged();
}
#Override
public void onInvalidated() {
super.onInvalidated();
mDataValid = false;
notifyDataSetChanged();
}
}
}
Well, I found out why this was happening and the reason is weird. The problem has nothing to do with the RecyclerView but with the way the data is fetch.
In my AsyncTask, where I fetch the data, I wrote a Log.d to print the Cursor size like so:
#Override
protected Void doInBackground(Void... Void) {
if(type==Constants.GET_ZIP_CODES)
cursor = db.getAllZipCodes();
else
cursor = db.searchZipCodes(text);
Log.d("DATABASE","SIZE "+cursor.getCount());
return null;
}
This made the AsyncTask take longer, the ProgressDialog takes longer to go off. What I understand is that somehow, the database query is performed, the code keeps compiling, but the data is only ready in the Cursor after a while. Once I printed the result just after the query, it didn't go past the line until the cursor was fully loaded.
Actually this is not an answer(Would have put it in comment if i had enough reputation points) just a suggestion/case study i came across while loading data from database to recyclerView. Instead of directly sending the cursor over to adapter i sent it as an arraylist, but thats besides the point.
The place where i got the freeze like you seem to get is when i have to load a text with over 700-800 character into a card. So when i crop the text to less than 600 the freeze vanishes.
So just check if you have any data with large character set, if so try removing that and test it out.
Hope it works for you, suggestion put read more option for large text like whatsapp does!

SearchView doesn't work with RecyclerView & SQLite

I have a Fragment with a RecyclerView. users input data and they will store in a SQLite DataBase. i am trying to Search in the items of this RecyclerView but it does not work,
here is my Fragment :
public class FragmentOne extends Fragment {
private RecyclerView mDetailRecyclerView;
private DetailAdapter mAdapter;
private boolean mNumberVisible;
private SearchView sv;
private ArrayList<Detail> mDetails=new ArrayList<>();
private View view;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
#Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.fragment_one_layout,
container, false);
mDetailRecyclerView = (RecyclerView) view.findViewById(R.id.detail_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
layoutManager.setReverseLayout(true); //This will reverse the data order but not scroll the RecyclerView to the last item
layoutManager.setStackFromEnd(true); //For keeping data order same and simply scrolling the RecyclerView to the last item
mDetailRecyclerView.setLayoutManager(layoutManager);
if (savedInstanceState != null) {
mNumberVisible =
savedInstanceState.getBoolean(SAVED_NUMBER_VISIBLE);
}
sv = (SearchView) view.findViewById(R.id.sv);
mAdapter = new DetailAdapter(mDetails);
sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit (String query) {
return false;
}
#Override
public boolean onQueryTextChange(String query) {
getDetailsSearch(query);
return false;
}
});
initViews();
updateUI();
return view;
}
#Override
public void onResume() {
super.onResume();
updateUI();
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(SAVED_NUMBER_VISIBLE, mNumberVisible);
}
..
..
private class DetailHolder extends RecyclerView.ViewHolder
implements View.OnClickListener, View.OnLongClickListener {
private TextView mTitleTextView;
// private TextView mDateTextView;
private Detail mDetail;
private RatingBar mRatingBar;
public DetailHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater.inflate(R.layout.list_item_detail,
parent, false));
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
mTitleTextView = (TextView) itemView.findViewById(R.id.detail_title);
mRatingBar = (RatingBar) itemView.findViewById(R.id.ratingBar);
}
public void bind(Detail detail) {
mDetail = detail;
mTitleTextView.setText(mDetail.getTitle());
mRatingBar.setRating(mDetail.getRate());
}
#Override
public void onClick(View view) {
Intent intent = DetailPagerActivity.newIntent(getActivity(),
mDetail.getId());
startActivity(intent);
}
}
private class DetailAdapter extends RecyclerView.Adapter<DetailHolder> {
private List<Detail> mDetails;
private Detail mDetail;
public DetailAdapter(List<Detail> details) {
mDetails = details;
}
#Override
public DetailHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
return new DetailHolder(layoutInflater, parent);
}
#Override
public void onBindViewHolder(DetailHolder holder, int position) {
Detail detail = mDetails.get(position);
holder.bind(detail);
}
#Override
public int getItemCount() {
return mDetails.size();
}
public void setDetails(final List<Detail> details) {
mDetails = details;
}
..
..
}
public void initViews(){
mDetailRecyclerView.setAdapter(mAdapter);
initSwipe();
}
..
..
private void getDetailsSearch (String searchTerm) {
mDetails.clear();
DBAdapter db = new DBAdapter(getActivity());
db.openDB();
Detail p = null;
Cursor c = db.retrieve(searchTerm);
while (c.moveToNext()) {
String title = c.getString(2);
p = new Detail();
p.setTitle(title);
mDetails.add(p);
}
db.closeDB();
mDetailRecyclerView.setAdapter(mAdapter);
}
}
and this is my Database Adapter:
public class DBAdapter {
Context c;
SQLiteDatabase db;
DetailBaseHelper helper;
public DBAdapter (Context c) {
this.c = c;
helper = new DetailBaseHelper(c);
}
public void openDB() {
try {
db = helper.getWritableDatabase();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void closeDB() {
try {
helper.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Cursor retrieve (String searchTerm) {
String[] columns = {
"_id",
"uuid",
"title",
"des",
"date",
"rate"
Cursor c = null;
if (searchTerm != null && searchTerm.length()>0) {
String sql ="SELECT * FROM " +DetailDbSchema.DetailTable.NAME+
" WHERE "+DetailDbSchema.DetailTable.Cols.TITLE+
" LIKE '%"+searchTerm+"%'";
c = db.rawQuery(sql, null);
return c;
}
c = db.query(DetailDbSchema.DetailTable.NAME, columns,
null, null, null, null, null);
return c;
}
}
and here is DataBase Helper:
public class DetailBaseHelper extends SQLiteOpenHelper{
private static final int VERSION = 1;
private static final String DATABASE_NAME = "detailBase.db";
public DetailBaseHelper (Context context) {
super(context, DATABASE_NAME, null, VERSION);
}
#Override
public void onCreate (SQLiteDatabase db) {
db.execSQL("create table " + DetailTable.NAME +
"(" +
" _id integer primary key autoincrement," +
DetailTable.Cols.UUID + ", " +
DetailTable.Cols.TITLE + ", " +
DetailTable.Cols.DES + ", " +
DetailTable.Cols.DATE + ", " +
DetailTable.Cols.RATE +
")"
);
}
#Override
public void onUpgrade (SQLiteDatabase db,
int oldVersion, int newVersion) {
}
}
here is the tutorial that i used for this,
I'll be appreciate if u have any idea for helping me.
I think that the main problem is here you're changing the adapter, but the new adapter was never modified by the data of the results, and also you have to notify your recycler that the data set changed. so
private void getDetailsSearch (String searchTerm) {
mDetails.clear();
/// the loop wiith the cursor
/// change the dataset
mAdapter = new DetailAdapter(mDetails);
mDetailRecyclerView.setAdapter(mAdapter);
/// tell the recycler there is a different data to display
mDetailRecyclerView.notifyDataSetChanged();
}

Android 4.0.3 CursorAdapter doesn't populate ListView on changeCursor

EDIT: I didn't post my XML for this dialog.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/tag_layout"
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_width="#dimen/min_dialog_width"
android:padding="5dp"
android:animateLayoutChanges="true"
>
<!-- Here is the view to show if the list is emtpy -->
<TextView
android:id="#android:id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_centerInParent="true"
android:gravity="center"
android:text="#string/no_items"
android:visibility="invisible"
/>
<ListView
android:id="#android:id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
/>
<ProgressBar
android:id="#+id/tag_spin_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
/>
</RelativeLayout>
I am using the android.support.v4.CursorLoader and CursorAdapter and I am trying to get it to update its cursor. In Android 2.3.3 it works just fine. However when I try it on my 4.0.3 device the ListView doesn't refresh and the newView method in my adapter is never called. I know the cursor has data in it since I can see it on my 2.3.3 device.
If I rotate my device the ListView shows what I want. I have tried invalidating the ListView but that doesn't solve the issue.
If I don't reset the ListView's adapter the list doesn't go blank, but it still doesn't refresh the list.
I am doing all of this inside of an extended AlertDialog that is embedded in a DialogFragment.
Here is the entire class
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.*;
import org.lds.ldssa.service.MLDatabase;
import org.lds.ldssa.service.aws.Annotation;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class TagDialog extends AlertDialog implements LoaderManager.LoaderCallbacks<Cursor>,AdapterView.OnItemClickListener {
private static final String TAG = "ldssa.tagdialog";
public static final int TAGLOADERID = 0;
// View Items
private EditText mEditText;
private ListView mListView;
private TextView mEmptyView;
private ProgressBar mProgressBar;
private ImageButton mNewTagButton;
private ImageButton mSortTagButton;
private TextView mTitle;
private String mTagTitle;
private String mNewTagTitle;
private Annotation mAnnotation;
private ContentFragment mContentFragment;
private boolean isNewTagView;
private static final String KEY_NEWTAGVIEW = "new_tag_view";
private static final String POSITION_KEY = "TAG_POSITION";
private static final String Y_KEY = "TAG_Y";
private static final String SORT_KEY = "TAG_SORT";
private static final String CHECKED_STATE_KEY = "TAG_CHECKED_STATE";
private static final int NOT_SET = -1;
private int mPosition;
private int mY;
private TagSuggestionAdapter mSuggestionAdapter;
private TagListAdapter mTagAdapter;
private MLDatabase mlDatabase;
private boolean mSortAlpha;
private HashMap<Long, CheckedState> mCheckedState;
protected TagDialog(Context context) {
super(context);
}
public void onCreate(Bundle savedInstanceState){
Context context = getContext();
Resources r = context.getResources();
final LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.dialog_tag, null);
// Main parts of the view
mEditText = (EditText) view.findViewById(R.id.tag_new_tag);
mListView = (ListView) view.findViewById(android.R.id.list);
mProgressBar = (ProgressBar) view.findViewById(R.id.tag_spin_progress_bar);
mEmptyView = (TextView) view.findViewById(android.R.id.empty);
mEmptyView.setVisibility(View.INVISIBLE);
// Titlebar
View titleBar = inflater.inflate(R.layout.dialog_tag_title, null);
mNewTagButton = (ImageButton) titleBar.findViewById(R.id.tag_new_icon);
mSortTagButton = (ImageButton) titleBar.findViewById(R.id.tag_sort_icon);
mTitle = (TextView) titleBar.findViewById(R.id.tag_title);
mTagTitle = r.getString(R.string.tag_dialog_title);
mNewTagTitle = r.getString(R.string.tag_new_dialog_title);
this.setCustomTitle(titleBar);
// Buttons
final String OK = r.getString(R.string.ok);
final String CANCEL = r.getString(R.string.cancel);
this.setButton(BUTTON_POSITIVE, OK, new OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) { /*Never Used*/}});
this.setButton(BUTTON_NEGATIVE, CANCEL, new OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) { /*Never Used*/}});
// Setup Button Listeners
setOnShowListener(new OnShowListener() {
#Override
public void onShow(DialogInterface dialog) {
Button ok = getButton(BUTTON_POSITIVE);
ok.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(isNewTagView){
hideIMM();
addNewTag();
mEditText.setText("");
setupTagDialog();
} else {
Collection<CheckedState> changes = mCheckedState.values();
boolean success = true;
MLDatabase db = getDatabase();
db.beginAnnotationTransaction();
for(CheckedState change : changes){
if(!change.checked()){
//Detag
db.detagAnnotation(mAnnotation.getDbKey(), change.tagID());
} else {
mAnnotation.saveHighlightsToDatabase(db);
if(mAnnotation.getDbKey().intValue() != MLDatabase.NOT_SET_INT &
change.tagID() != MLDatabase.NOT_SET_INT){
success = db.tagAnnotation(mAnnotation.getDbKey(), change.tagID(), change.changed());
}
}
}
if(success){
db.setAnnotationTransactionSuccessful();
}
db.endAnnotationTransaction();
mCheckedState.clear();
dismiss();
}
}
});
Button cancel = getButton(BUTTON_NEGATIVE);
cancel.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(isNewTagView){
hideIMM();
setupTagDialog();
mEditText.setText("");
} else {
mCheckedState.clear();
dismiss();
}
}
});
}
});
mNewTagButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
setupNewTagDialog();
}
});
mSortTagButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mSortAlpha = !mSortAlpha;
restartLoader();
}
});
mListView.setOnItemClickListener(TagDialog.this);
mEditText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
#Override
public void afterTextChanged(Editable s) {
LoaderManager lm = getLoaderManager();
if(lm != null){
Loader l = lm.getLoader(TAGLOADERID);
if(l != null){
l.forceLoad();
} else {
restartLoader();
}
} else {
restartLoader();
}
}
});
//Handle Rotations
if(savedInstanceState == null){
//New
mPosition = NOT_SET;
mY = NOT_SET;
mSortAlpha = false;
mCheckedState = getCheckedState(mAnnotation.getDbKey());
isNewTagView = false;
} else {
//rotated
isNewTagView = savedInstanceState.getBoolean(KEY_NEWTAGVIEW, false);
mPosition = savedInstanceState.getInt(POSITION_KEY, NOT_SET);
mY = savedInstanceState.getInt(Y_KEY, NOT_SET);
mSortAlpha = savedInstanceState.getBoolean(SORT_KEY, false);
restoreCheckedState(savedInstanceState);
}
mTagAdapter = new TagListAdapter(context, null, mCheckedState);
mSuggestionAdapter = new TagSuggestionAdapter(context, null, 0);
LoaderManager lm = getLoaderManager();
if(lm != null){
lm.initLoader(TAGLOADERID, null, this);
}
this.setView(view);
super.onCreate(savedInstanceState);
}
private void addNewTag() {
String tag = mEditText.getText().toString().trim();
if(!tag.equals("")){
getDatabase();
Integer langID = mAnnotation.getLanguageId();
try{
long tagID = mlDatabase.insertTag(langID, tag);
if(mAnnotation.getDbKey().intValue() != MLDatabase.NOT_SET_INT &&
tagID != MLDatabase.NOT_SET_INT){
mCheckedState.put(tagID, new CheckedState(tagID, true, true));
}
} catch (Exception e) {
Log.d(TAG, "Problem saving new tag: " + tag + " : " + e.getMessage());
e.printStackTrace();
}
}
}
public void onStart(){
if(isNewTagView){
setupNewTagDialog();
} else {
setupTagDialog();
}
restartLoader();
}
#Override
public Bundle onSaveInstanceState(){
Bundle bundle = super.onSaveInstanceState();
//Save What dialog we are in.
bundle.putBoolean(KEY_NEWTAGVIEW, isNewTagView);
bundle.putBoolean(SORT_KEY, mSortAlpha);
//Save position
bundle.putInt(POSITION_KEY, mListView.getFirstVisiblePosition());
final View v = mListView.getChildAt(0);
bundle.putInt(Y_KEY, (v == null) ? 0 : v.getTop());
//Save Checked State
Iterator it = mCheckedState.entrySet().iterator();
int i = 0;
while(it.hasNext()){
Map.Entry pair = (Map.Entry)it.next();
bundle.putSerializable(CHECKED_STATE_KEY + i, (CheckedState)pair.getValue());
i++;
}
bundle.putInt(CHECKED_STATE_KEY, i);
return bundle;
}
private void restoreCheckedState(Bundle bundle){
int count = bundle.getInt(CHECKED_STATE_KEY);
mCheckedState = new HashMap<Long, CheckedState>();
boolean success = true;
for(int i = 0; i < count; i++){
CheckedState cs = (CheckedState)bundle.getSerializable(CHECKED_STATE_KEY+i);
if(cs == null){
success = false;
break;
}
mCheckedState.put(cs.tagID(), cs);
}
if(!success){
mCheckedState = getCheckedState(mAnnotation.getDbKey());
}
}
#Override
public void onBackPressed(){
if(isNewTagView){
hideIMM();
setupTagDialog();
} else {
this.dismiss();
}
}
private void setupTagDialog() {
isNewTagView = false;
mTitle.setText(mTagTitle);
mNewTagButton.setVisibility(View.VISIBLE);
mSortTagButton.setVisibility(View.VISIBLE);
mEmptyView.setVisibility(View.INVISIBLE);
mEditText.setVisibility(View.GONE);
mListView.setVisibility(View.GONE);
mProgressBar.setVisibility(View.VISIBLE);
mListView.setAdapter(mTagAdapter);
restartLoader();
}
private void setupNewTagDialog() {
isNewTagView = true;
mTitle.setText(mNewTagTitle);
mNewTagButton.setVisibility(View.INVISIBLE);
mSortTagButton.setVisibility(View.INVISIBLE);
mEmptyView.setVisibility(View.INVISIBLE);
mEditText.setVisibility(View.VISIBLE);
mListView.setVisibility(View.GONE);
mProgressBar.setVisibility(View.VISIBLE);
mListView.setAdapter(mSuggestionAdapter);
restartLoader();
}
public void setAnnotation(Annotation a) {
mAnnotation = a;
}
public void setContentViewInterface(ContentFragment contentFragment) {
mContentFragment = contentFragment;
}
private MLDatabase getDatabase() {
if(mlDatabase == null){
GospelLibraryApplication app = (GospelLibraryApplication) getContext().getApplicationContext();
mlDatabase = app.getMlDatabase();
}
return mlDatabase;
}
public String getFilter() {
return mEditText.getText().toString().trim();
}
public Integer getAnnotationID(){
if(mAnnotation != null){
return mAnnotation.getDbKey();
}
return MLDatabase.NOT_SET_INT;
}
private LoaderManager getLoaderManager(){
if(mContentFragment == null){
Log.d(TAG, "ContentFragment is NULL!");
return null;
}
return mContentFragment.getContentActivity().getSupportLoaderManager();
}
private void restartLoader(){
LoaderManager lm = getLoaderManager();
if(lm != null){
lm.restartLoader(TAGLOADERID, null, this);
}
}
private void hideIMM(){
InputMethodManager imm = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
}
private HashMap<Long, CheckedState> getCheckedState(Integer annotationID) {
HashMap<Long, CheckedState> checkedState = new HashMap<Long, CheckedState>();
MLDatabase db = getDatabase();
Cursor cursor = db.queryAllTagsWithAnnotation(annotationID);
if(cursor != null){
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()){
Long tagID = cursor.getLong(cursor.getColumnIndex(MLDatabase.CL_ID));
boolean isChecked = !cursor.isNull(cursor.getColumnIndex(MLDatabase.CL_ANNOTATION));
checkedState.put(tagID, new CheckedState(tagID, isChecked, false));
}
}
return checkedState;
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
TagCursorLoader loader = new TagCursorLoader(getContext(), this);
return loader;
}
#Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) {
if(isNewTagView) {
mSuggestionAdapter.changeCursor(data);
if(mListView.getAdapter() == null){
mListView.setAdapter(mSuggestionAdapter);
}
} else {
mTagAdapter.changeCursor(data);
if(mListView.getAdapter() == null){
mListView.setAdapter(mTagAdapter);
}
}
if(mPosition != NOT_SET && mY != NOT_SET){
mListView.setSelectionFromTop(mPosition, mY);
mPosition = mY = NOT_SET;
}
if (mListView.getAdapter() != null) {
if (mListView.getAdapter().getCount() > 0) {
mEmptyView.setVisibility(View.INVISIBLE);
}
else {
mEmptyView.setVisibility(View.VISIBLE);
}
}
else {
mEmptyView.setVisibility(View.VISIBLE);
}
mProgressBar.setVisibility(View.GONE);
mListView.setVisibility(View.VISIBLE);
mListView.invalidate();
}
#Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
if(mSuggestionAdapter != null) {
mSuggestionAdapter.changeCursor(null);
}
}
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if(isNewTagView){
TextView tv = (TextView)view;
mEditText.setText(tv.getText());
Button ok = getButton(BUTTON_POSITIVE);
if(ok != null){
ok.performClick();
}
} else {
CheckedTextView ctv = (CheckedTextView)view;
boolean checked = !ctv.isChecked();
ctv.setChecked(checked);
mCheckedState.put(id, new CheckedState(id, checked, true));
}
}
public static class TagCursorLoader extends CursorLoader {
private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
private TagDialog dialog;
private MLDatabase mlDatabase;
private Cursor mCursor;
private String mFilter;
private Integer mAnnotationID;
// Runs on worker thread
#Override
public Cursor loadInBackground(){
Cursor cursor = null;
if(dialog.isNewTagView){
mFilter = dialog.getFilter();
cursor = mlDatabase.getTagSuggestions(mFilter);
} else {
cursor = mlDatabase.queryTags(dialog.mSortAlpha);
}
if(cursor != null){
cursor.registerContentObserver(mObserver);
}
return cursor;
}
//Runs on UI thread
#Override
public void deliverResult(Cursor cursor){
//Handle if canceled in the middle.
if(isReset()){
if(cursor != null){
cursor.close();
}
return;
}
Cursor oldCursor = mCursor;
mCursor = cursor;
if(isStarted()) {
super.deliverResult(cursor);
}
if(oldCursor != null && !oldCursor.equals(cursor) && !oldCursor.isClosed()) {
oldCursor.close();
}
}
public TagCursorLoader(Context context, TagDialog dialog) {
super(context);
this.dialog = dialog;
mlDatabase = dialog.getDatabase();
}
#Override
public void onStartLoading(){
if(mCursor == null) {
forceLoad();
} else {
if(dialog.isNewTagView && mFilter.equals(dialog.getFilter())) {
deliverResult(mCursor);
} else {
forceLoad();
}
}
}
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
#Override
public void onCanceled(Cursor cursor) {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
}
}
/**
* Class is used to store the temporary checked state of the tags.
*/
public class CheckedState implements Serializable {
private static final long serialVersionUID = 1263560458217339487L;
/**
* #serialField
*/
private long tagID;
/**
* #serialField
*/
private boolean checked;
/**
* #serialField
*/
private boolean changed;
/**
* Constructor for CheckedState.
* #param tagID The tag ID
* #param checked The Current Checked State
* #param changed Ture if changed in the dialog. False if pulling from database.
*/
public CheckedState(long tagID, boolean checked, boolean changed){
this.tagID = tagID;
this.checked = checked;
this.changed = changed;
}
public long tagID(){
return tagID;
}
public boolean checked() {
return checked;
}
public boolean changed() {
return changed;
}
}
}
Note I didn't add my XML before.
That is where the issue resides.
andriod:animateLayoutChanges
doesn't work with what I was trying to do.
Once I removed that from my XML it worked like a charm.
In most examples that I see, the you create your adapter instance once and set it into the ListView when you view is created, and then call getLoaderManager().initLoader().
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
Then in the onLoadFinished() method you call swapCursor() which automatically refreshes the ListView.
// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
The above code was copied from the Loaders documentation
http://developer.android.com/guide/topics/fundamentals/loaders.html
UPDATE:
The documentation talks about using Loaders for Activities and Fragments, but doesn't mention using Dialogs. I'm guessing that if getLoaderManager() exists you are fine, but if you are not using the LoaderManager and you are running the Loader manually yourself, then I would think that you'd need to ensure that when you call swapCursor() or setAdapter() that you are doing this on the UI thread. Sometimes the easiest way to ensure this, is to call
getListView().post(new Runnable() {
public void run() {
// so the setAdapter() or swapCursor() here
}
});
I have run into cases myself where I've updated a ListView in the background and it doesn't reflect as being updated until I rotate the device, because the UI wasn't updated on the UI thread.

Categories

Resources