On screenrotation the selected items get unselected, I want to save its state.
I have all these forms in recyclerView which get selected on click. This is the code in onListItemClick
private void onListItemClick(View view, int position) {
Cursor cursor = instanceAdapter.getCursor();
cursor.moveToPosition(position);
CheckBox checkBox = view.findViewById(R.id.checkbox);
checkBox.setChecked(!checkBox.isChecked());
long id = cursor.getLong(cursor.getColumnIndex(InstanceProviderAPI.InstanceColumns._ID));
if (selectedInstances.contains(id)) {
selectedInstances.remove(id);
} else {
selectedInstances.add(id);
}
Bundle bundle=new Bundle();
sendButton.setEnabled(selectedInstances.size() > 0);
toggleButtonLabel();
}
where selectedInstances is a LinkedHashSet
private LinkedHashSet<Long> selectedInstances;
Here is the GIF
Unless you can store it in a database or anything that's 'persistent', then you could just keep a list/array of booleans with as many entries as your ListView. When you check the second checkbox, set array[1] = true.
Then in your adapter you just check the state of the position of the list for the current item.
Somewhat of an example
boolean[] checkedState = new boolean[list.count];
private void onListItemClick(View view, int position) {
//...
checkedState[position] = //checked state
}
//adapter
public void onBindViewHolder(#NonNull final RecyclerView.ViewHolder holder, int position) {
//...
checkBox.isChecked = checkedState[position]
}
ViewModel Stores UI-related data that isn't destroyed on app rotations.
Regarding to keep UI states you can use the methods onSaveInstanceState() and onRestoreInstanceState of activity object for save that data in the Bundle type parameter as follow bellow:
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
String setting;
state.putString("KeyName", setting);
}
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
String setting;
setting = state.getString("KeyName");
}
The most correct way according to Android Developer Documentation's to persist that data, so when the app's finished and then it's started again the app will can retrieve that data from local storage.
It can be by SharedPreferences or Room database depending on data complexity. Like the data are settings, so I believe you'll want to store one when you had finished your app and to retrieve one when you start you app again.
References:
Saving UI States
SharedPreferences
Save key-value data
Save data in a local database using Room
RoomDatabase
Android Jetpack: Room
What’s New in Room (Android Dev Summit '19)
Related
I'm developing an application that display a list of items and allows the users to add more items and check the items' details.
Let's say that I keep my database references in the activity, adding ChildEventListener listeners onResume() and removing them on onPause():
public void onResume() {
super.onResume();
mItemChildEventListener = mDatabaseReference.child('items').addChildEventListener(new ChildEventListener() {
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
mAdapter.addItem(dataSnapshot.getValue(Item.class));
}
...
};
}
public void onPause() {
super.onPause();
if (mItemChildEventListener != null) {
mDatabaseReference.removeEventListener(mItemChildEventListener);
}
}
In these ChildEventListener, I'm adding item by item to the list of items and calling notifyItemInserted(itemArray.size() - 1) in the adapter.
public void addItem(Item item) {
if (mItems == null) {
mItems = new ArrayList<>();
}
mItems.add(item);
notifyItemInserted(mItems.size() - 1);
}
Also, to keep the application running smoothly, I'm keeping the list of items in a Reteiner Fragment, saving this values onSaveInstanceState() and restoring them on onCreate().
protected void onCreate(#Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
mItems = getState(STATE_ITEMS);
} else {
mItems = new ArrayList<>();
}
mAdapter = new ItemAdapter(mItems);
}
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveState(STATE_ITEMS, mItems);
}
public <T> T getState(String key) {
//noinspection unchecked
return (T) mRetainedFragment.map.get(key);
}
public void saveState(String key, Object value) {
mRetainedFragment.map.put(key, value);
}
public static class RetainedFragment extends Fragment {
HashMap<String, Object> map = new HashMap<>();
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
}
However, if I unregister and register the listener again, for example, by any orientation change or leaving and returting to the activity, as expected, onResume() will be called, and by adding the ChildEventListener again, the same data is retrieved, which, in case if it's not compared with the data that is already contained in the list of items, mItems, it will be duplicated.
Also, if I do not keep the list of items, mItems, and retrieve the data every time onResume() is called, the list loses its scroll position and also blinks, since it takes some time to retrieve the data.
Question
How can I keep the list of items retrieved without retrieving them again again, but keep listening for changes in the items already retrieved and for new items added? Is there a better solution than comparing the items already retrieved with the new items, for example, by their key?
I've tried looking in the documentation and other questions, but I couldn't find any information relevant for this situation. Also, I have tried to keep the reference to mDatabaseReference in onSaveInstanceState(), as I have done with the list of items mItems, but it has't worked either.
If you enable persistence in the Firebase Realtime Database SDK, that will help prevent unnecessary fetches of data that's already been received. The SDK will cache fetch data locally and prefer to use that data first when it's available:
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
Be sure to read the documentation to understand how this works.
Also bear in mind that it's traditional to start and stop listener during onStart and onStop. This will avoid even more refetches if the app loses focus for a moment (for example, it pops up a dialog, or some other transparent activity appears on top).
Just make a Firebase call inside a Loader and save the data in a static variable or list or whatever data structure suits you.
Initialize the Loader in onCreate.
In the Loader, override the onStartLoading() method and in onStart() call this method.
Inside onStartLoading() simply check if the static variable is null or not. If it is null, startLoading. Else do not load, and set the previous data as data source.
The advantage of using Loaders is, in case of orientation changes, it won't make network calls as AsyncTask does.
I have a fragment X which indeed has a RecyclerView, X has a search view, I use the search view to search something and filter the RecyclerView into few rows. After the filtering, if user clicks on some row, it goes to another fragment say Y. From there if the user clicks back it comes back to X. My task is that X should persist the search results after this coming back. What is the best approach to achieve this?
You can use a the singleton pattern to store the data!
E.g.
// DataManager.java
public class DataManager {
private static DataManager thisInstance;
// Declare instance variables
List<String> searchResultItems;
public static DataManager getSharedInstance() {
if (thisInstance == null) {
thisInstance = new DataManager();
}
return thisInstance;
}
private DataManager() {
searchResultItems = new ArrayList<>();
}
public List<String> getSearchResultItems() {
return searchResultItems;
}
public void setSearchResultItems(List<String> searchResultItems) {
this.searchResultItems = searchResultItems;
}
}
Now you can store and retrive data from everywhere:
// Setter
DataManager.getSharedInstance().setSearchResultItems(items);
// Getter
List<String> items= DataManager.getSharedInstance().getSearchResultItems();
Propertly override onSaveInstanceState in Fragment so that it will store search input - filter. Also override onCreate in such way it will apply saved filter on your RecyclerView.
Before navigating to another fragment, obtain Fragment.SavedState via FragmentManager and save it temporary in Activity which hosts your fragments. Note, this state can be lost if you do not properly save Activity state due of configuration changes (rotate) = you have to override also onSaveInstanceStatein Activity. Or simply save Fragment.SavedState in global scope (some static field, or in Application).
When navigating back to previous fragment, re-create fragment from Fragment.SavedState i. e. invoke Fragment#setInitialSavedState(Fragment.SavedState).
For more details see my research on similar topic.
I'm currently writing a major upgrade to my language teaching App, and wanted to use SharedPreferences in order to store user progress. Basically, a recyclerview displays a list of topics, which lead to exercises -complete 5 exercises, and the topic is finished.
At this point, I call saveProgress(), and store the topic number and an int representing completion (1) in sharedPreferences.
However, how and where in the recylclerView code should I call the check for a topic being completed? Currently in my bindData method of my ViewHolder inner class, I have:
SharedPreferences sp = getActivity().getSharedPreferences("phraseprefs", Context.MODE_PRIVATE);
//the number topic serves as the key name of the preference
int completed = sp.getInt(Integer.toString(mTopic.topicRef), 0);
if (completed ==1){
//we check if exercise completed, if so, make view greyed out
float alpha= 0.65f;
nameTextView.setAlpha(alpha);
nameTextView.setBackgroundColor(Color.GREY);
itemView.setAlpha(alpha);
itemView.setBackgroundColor(Color.GREY);
}
This doesnt seem to work however. Any tips would be appreciated!
EDIT: Added in SaveProgress() method below, note that "topic" in this context is a number from 0-xxx, at increments of 5 per topic (i.e. topic 0,5,10, with exercises 0-4,5-9,10-14, respectively)
public void saveProgress(){
SharedPreferences sp = getActivity().getSharedPreferences("phraseprefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
//we use 1 to represent completion instead of boolean because we can also sum these numbers
//for a completion percentage if necessary
editor.putInt(Integer.toString(topic), 1);
editor.commit();
}
EDIT2: Solved by a mixture of trial and error and magic.
First, initialize the textViews in ViewHolder constructor.
Second, Fetch sharedPreferences in OnBindViewHolder
Third, call methods to change the textViews within onBindViewHolder, provided the shared Preference meets your conditions.
Thank you all
I think RecyclerView shouldn't know about SharedPreferences at all. You probably have a model class Topic which contains all the information about a topic. You can include your completion int in this class. Something like this.
class Topic {
/* all topic data */
private int completion;
public int getCompletetion() {
return compeletion;
}
public void setCompletion(int completion) {
this.completion = completion;
}
public Topic(/*Other Topics fields*/) {
}
public Topic(/*Other Topic fields*/, int completion) {
/* initialization of other fields */
this.completion = completion;
}
}
Then you can load and set a completion int for each Topic in your array (I mean List or whatever container you use). Something like this
List<Topic> topicsList = new ArrayList<>();
/* add your topics to the list from whatever source */
for (Topic topic : topicsList) {
topicsList.setCompletion(loadCompletion(topic));
}
YourAdapter adapter = new YourAdapter(topicsList)
/* ... */
Now, in order to check completion int for a Topic in your data array in your RecyclerView you can just call topic.getCompletion(). It would look something like this
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
Topic topic = topicsList.get(position);
int completion = topic.getCompletion();
switch (completion) {
case '0':
holder.modifyViewForCompletionZero();
break;
case '1':
holder.modifyViewForCompletionOne();
break;
/* Call holder methods for other completion states */
}
}
And your inner ViewHolder would contain methods for adjusting view for each state.
If i had to send a request to get data within onBindViewHolder(), how can i update view after data comes back from server?
what I have now is that I cache the data along with row position, so that next time when user scrolls to that row, I can display info right away.
but there are 2 other issues I don't know how to solve.
I scroll the list to view item at position 10, 11 and 12. I decided to wait for data to come back. do i call notifyDataSetChanged() after? because onBindViewHolder already been called by the time data comes back and view would just remained empty, but I also don't think by calling notifyDataSetChanged() after each request would be a good idea.
I start to view the list at position 0 and keep scrolling to position 10. app sends out request to pull data for position 0 to 10. since the request at 0 is sent out first, more likely it would get back first or at least sooner than position 10, but by that time I'm already viewing the item at position 10. my view would start changing if all requests are back in order, so it would show data for position 0 then keep updating all the way to 10.
is it a bad practice to load data from server as recyclerview scrolls? but by doing this would save me a lot of time, and I guess for user too? because instead of sending all the requests ahead of time, user get to see partial data while other data are being loaded in the background.
Thanks!!!
EDITED
public class TestAdapter extends RecyclerView.Adapter<TestAdapter.ViewHolder> {
private Context ctx;
private ArrayList<Photo> alPhotos = new ArrayList<>();
private HashMap<String, Drawable> hmImages = new HashMap<>();
public TestAdapter(Context ctx, ArrayList<Photo> alPhotos) {
this.ctx = ctx;
this.alPhotos = alPhotos;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photo_brief, parent, false));
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Photo photo = alPhotos.get(position);
loadRemoteImage(photo.IMG, holder.ivThumb, true);
holder.tvEmail.setText(photo.EMAIL);
}
#Override
public int getItemCount() {
return alPhotos.size();
}
private void loadRemoteImage(final String imgUrl, final ImageView view, final boolean cache) {
if (hmImages.containsKey(imgUrl)) {
view.setImageDrawable(hmImages.get(imgUrl));
} else {
final WeakReference<ImageView> weakView = new WeakReference<>(view);
RequestManager.getManager().add(new Request<>(imgUrl, new DrawableParser(), new RequestCallback<Drawable>() {
#Override
public void onFinished(Request<Drawable> request, Response<Drawable> response) {
if (cache) hmImages.put(imgUrl, response.result);
ImageView view = weakView.get();
if (view != null) {
view.setImageDrawable(response.result);
}
}
#Override
public void onError(Request<Drawable> request, Response<Drawable> response) {
}
#Override
public void onTimeout(Request<Drawable> request, Response<Drawable> response) {
}
})
);
}
}
public class ViewHolder extends RecyclerView.ViewHolder {
private ImageView ivThumb;
private TextView tvEmail;
public ViewHolder(View itemView) {
super(itemView);
findViews();
}
private void findViews() {
ivThumb = (ImageView) itemView.findViewById(R.id.ivThumb);
tvEmail = (TextView) itemView.findViewById(R.id.tvEmail);
}
}
}
It's a good idea to load data while scrolling, a lot of popular apps do that. I personally use WeakReference in this case. I store a weak reference to the view holder in my model when I start loading data. If the reference is still valid by the time I get the response then it makes sense to update the view. If there is no view holder in memory then it's already been recycled and I don't have to update anything anymore.
When onViewRecycled is called you can clear the weak reference and also consider cancelling the network request (depends on your needs).
Caching works perfect with this model, you just insert this logic before making a network request. Again, this depends on your needs, maybe you don't need caching at all, or maybe your data is rarely updated then it makes sense to always use cache until some event.
In my app I also use EventBus, it helps me with event handling, but it is absolutely fine to just use Android SDK and support library.
You can also add a ScrollListener if you need to differentiate the item behavior depending on whether user scrolls the list right now. E.g. in my app I animate the data if list loaded and user wasn't scrolling it (improves interaction with the user). When user scrolls I load data as is, because it will be too much motion on the screen if I animate data.
I'm building an android app similar to Facebook (gets a newsfeed stored in db from a REST API), and I am now implementing a reddit-like voting system, in which every feed item has a vote state (none, up, down) stored in db.
When I do retrieve the newsfeed json, i set its voteState to the vote state in the json, then I display it through a newsfeed adapter.
But if i set the voteState in the newsfeed adapter to another value (based on an onClickListener), this changed value does not reach the actual newsfeed.
This is because every time I scroll the newsfeed, the newsfeed adapter gets a new instance of the newsfeed, and doesn't care about the value I changed.
Thus, I'm looking for the best way to modify permanently the newsfeed vote state from the newsfeed adapter (this can be generalized to any variable assigned to an ArrayList and then displayed through an ArrayListAdapter).
If you have any suggestions, please feel free to respond.
Thank you :)
EDIT :
public class NewsfeedAdapter extends RecyclerView.Adapter<NewsfeedAdapter.ViewHolder> {
private Newsfeed newsfeed;
private FeedItem item;
public NewsfeedAdapter(..., Newsfeed newsfeed) {
...;
this.newsfeed = newsfeed;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected Button button_up/down....
public ViewHolder(View v) {
super(v);
button = findViewById ... (R.id.button);
...
}
}
// Create new views (invoked by the layout manager)
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
...
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
final FeedItem item = newsfeed.get(position);
holder.button_up.setTag(position);
holder.button_up.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
changeVote(item, "up");
}
});
holder.button_down.setTag(position);
holder.button_down.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
changeVote(item, "down");
}
});
}
public void changeVote(FeedItem item, String flag) {
// Log.d(TAG, "State of vote :" + item.getVoteState());
if (item.getVoteState().equals(flag)) {
item.setVoteState("none");
Log.d(TAG, "Deleting vote :" + item.getVoteState());
new Connexion(activity, fragment, "vote", "Deleting vote...").execute(String.valueOf(item.getId()), flag, "delete");
}
else if (item.getVoteState().equals("none")){
item.setVoteState(flag);
Log.d(TAG, "Voting :" + item.getVoteState());
new Connexion(activity, fragment, "vote", "Voting...").execute(String.valueOf(item.getId()), flag, "insert");
} else {
item.invertVoteState(flag);
Log.d(TAG, "Inverting :" + item.getVoteState());
new Connexion(activity, fragment, "vote", "Voting...").execute(String.valueOf(item.getId()), flag, "invert");
}
} }
You have two options:
1) Send updates to the rest API every time a piece of data is changed. This is really expensive and not something you want to do most likely.
2) Create a local data layer between your rest API and your listview - most likely an SQLite database or some other ORM lib. This way you can hold an instance of the DB in your adapter, and whenever a piece of data changes, you can update that through a simple db method call, and then periodically send updates to the rest API, while also checking for new updates from the rest API and then pushing them to the DB.
I would highly recommend using an SQLite DB, as you can keep a counter in your DB code of a certain number of changes, or time passed since last sync, and keep the data the user is seeing up to date with out smashing the users data usage with repeated network calls.