I have a ListView containing news from RSS feeds in MainActivity, the problem is I have to stream the RSS feeds everytime I open the app because the items of the ListView are destroyed when I close the app.
I understand I can save it in SQLite temporarily, but is there a more simple way to save the ListView layout just so it would still be there next time I open the app?
Another option is to use SharePreferences and Gson to convert your datasource for the list view into a string for storage and then when the app is re-opened you can rebuild the list view with the stored data fairly quickly. I do something similar in one app where the data source for my list view is a ArrayList of LinkedHashMap items, so this would be the two methods for converting the ArrayList to a String and then back to an ArrayList when needed
public static String ArrayListToString(ArrayList<LinkedHashMap> list) {
return gson.toJson(list);
}
public static ArrayList<LinkedHashMap> StringToArrayList(String input) {
Type collectiontype = new TypeToken<ArrayList<LinkedHashMap>>(){}.getType();
ArrayList<LinkedHashMap> list = gson.fromJson(input, collectiontype );
return list;
}
I would also then suggest storing a timestamp so you can check if the stored list should be displayed or if an updated list needs to be retrieved
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView)findViewById(R.id.clientListView)
...whatever other setup you want to do here
CheckTimeStamp();
}
public static void CheckTimeStamp() {
String timeStamp = preferences.getString("keyClientTimeStamp", "");
Calendar calendar = Calendar.getInstance();
Date date = calendar.getTime();
String currentTime = HelperClass.GetSimpleDateFormat(date);
if (currentTime.equals(timeStamp)) {
String storedString = preferences.getString("keyStoredClients", "");
clientArrayList = HelperClass.StringToArrayList(decryptedArray);
//the setupView method is where I take my ArrayList
//and add it to my ListView's Adapter
SetupView();
}
else {
SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.keySalesTimeStamp, currentTime);
editor.apply();
//for me this is a web service that get's a list of clients,
//converts that list to a String to store in SharedPreferences
//and then calls SetupView() to add the list to the ListView Adapter
GetClientList();
}
}
Hmm, I would use Volley library for caching requests, I guess it very simple: you do request and next time first of all get it from cache. You don't have to explicitly save and describe a model for storing data for this case.
Below, I gave the example of how it might look:
public class RSSProvider {
private final RequestQueue mRequestQueue;
// ...
private RSSProvider(Context context) {
mRequestQueue = Volley.newRequestQueue(context.getApplicationContext());
}
// ...
public void getSomething(final Response.Listener<String> responseListener, final Response.ErrorListener errorListener) {
if (mRequestQueue.getCache().get(<URL>) != null) {
responseListener.onResponse(new String(mRequestQueue.getCache().get(<URL>).data));
} else {
StringRequest request = new StringRequest(Request.Method.GET, <URL>, responseListener, errorListener);
request.setShouldCache(true);
mRequestQueue.add(request);
}
}
// ...
}
Related
I'm saving an arraylist on shared preferences but when i add something new to this array it deletes the old one and displays only the new one.
Here is the save and load array from shared preferences
//SHARED PREFERENCES Save ArrayList
public boolean saveArrayList(SharedListFood list) {
SharedPreferences.Editor editor = prefs.edit();
Gson gson = new Gson();
String json = gson.toJson(list.getMlist()); //put in json the list from my model(SharedFoodList) which is the list i provide(itemsAdded)
editor.putString("testShared", json);
return editor.commit(); // This line is IMPORTANT !!!
}
//SHARED PREFERENCES Load ArrayList
public ArrayList<String> getArrayList() {
ArrayList<String> loadArrayList;
Gson gson = new Gson();
String json = prefs.getString("testShared", null);
Type type = new TypeToken<ArrayList<String>>() {
}.getType();
loadArrayList = gson.fromJson(json, type);
return loadArrayList;
}
I add the item here.
searchList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
searchMessage = searchList.getItemAtPosition(position).toString(); //searchMessage gets the value of the pressed item in list
if(searchMessage.contains("two")){
Log.d("alekos","tak"+searchMessage);
}
Toast.makeText(AddFood.this, "" + searchMessage, Toast.LENGTH_SHORT).show();
itemsAdded.add(searchMessage);// made it static so it is created here but displayed in the AddFoodBasket.java
sharedArray=new SharedListFood(itemsAdded);
boolean isSuccess= sharedArrayPreferencesHelper.saveArrayList(sharedArray); //sends itemsAdded to saveArrayList in shared preferences
if (isSuccess) {
Toast.makeText(getApplicationContext(),"Personal information saved", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getApplicationContext(),"Personal information NOT", Toast.LENGTH_LONG).show();
}
}
});
Where itemsAdded is the arraylist i want to add each time
As per my understanding,
1. you have written SharedPreferences.Editor inside saveArrayList().
2. On every single time this method called, you create a new Editor and it replaces the
previous one.
3. SharedPreferences stores in key-value pair and you are storing data in the same key
every time. (It Replaces previous values with new ones)
4. Your code might be correct for data but the flow is wrong. Try to work on your code-
flow.
Hope it helps. :)
I have an activity that in onCreate() does the following:
Creates an empty ArrayList
Creates a new ArrayAdapter associated with the above ArrayList
Sets ListView to use the above ArrayAdapter
Uses Volley to send a GET request to my API to fetch some JSON data to load into the ListView
Once the data is fetched I add it to my ArrayList and the ListView is populated as expected
My problem is that when the activity is restarted (i.e. the screen is rotated via the emulator or the activity is restarted through Android Studio) the ListView no longer populates.
I am not saving any state. I expect the activity to return to its initial default state so I don't think onSaveInstanceState() is the answer.
I've verified that the data is returned successfully from the API and that the adapter's hashcode is the same before and after the volley request and that it equals the ListView's set adapter. I've also verified that onDestroy() and then onCreate() are called when the activity is restarted so I know it is going through a full life cycle.
If I rotate the screen programmatically with setRequestedOrientation() I don't experience this issue. If I add items to my ArrayList outside of the GET request callback, I don't experience this issue.
Here is my activity onCreate()
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
//The data to be displayed
descriptions = new ArrayList<>();
listView = (ListView)this.findViewById(R.id.myListView);
//Link 'descriptions' to the adapter
adapter = new ArrayAdapter<>(this, R.layout.list_json_text_view, descriptions);
listView.setAdapter(adapter);
this.addTextFilter();
this.addListViewClickListener();
//See my ApiGetRequest class below
request = new ApiGetRequest();
request.send(this.getContext(), getDataUrl(), this, "", REQUEST_TYPES.TEXT);
}
And my activity GET request callback
public void onSuccess(DescriptiveJSONArray items, REQUEST_TYPES type) {
descriptions.clear();
try {
for (int i = 0; i < items.length(); ++i) {
JSONObject obj = items.getJSONObject(i);
String desc = obj.optString("name", "") + " " + obj.optString("description", "");
//TODO: Remove debug code
System.out.println("Adding: "+desc);
descriptions.add(desc);
}
}
catch(JSONException e) {
e.printStackTrace();
//getJSONObject failed
}
}
And my ApiGetRequest methods
//My activity implements ApiGetCallback
public void send(Context context, String url, ApiGetCallback callback, String tag, REQUEST_TYPES type) {
StringRequest stringRequest = getStringRequest(url, callback, tag, type);
//Singleton wrapper for RequestQueue
AppRequestQueue queue = AppRequestQueue.getInstance(context);
queue.add(stringRequest);
}
//Inner class inside ApiGetCallback
class SuccessListener implements Response.Listener<String> {
ApiGetCallback callback;
REQUEST_TYPES type;
public SuccessListener(ApiGetCallback callback, REQUEST_TYPES type) {
this.callback = callback;
this.type = type;
}
#Override
public void onResponse(String response) {
try {
DescriptiveJSONArray jsonResp = new DescriptiveJSONArray(response);
callback.onSuccess(jsonResp, type);
}
catch(JSONException e) {
callback.onJsonException(e);
}
}
}
Any ideas what is happening?. I'm testing on Marshmallow and Nougat
You are missing a call to notifyDataSetChanged, after the onSuccess function is done.
you may need to override onStart and do update anything in it
adapter = new ArrayAdapter<>(this, R.layout.list_json_text_view, descriptions);
listView.setAdapter(adapter);
//See my ApiGetRequest class below
request = new ApiGetRequest();
request.send(this.getContext(), getDataUrl(), this, "", REQUEST_TYPES.TEXT);
use this part of code in onResume method.
Here is my issue. I can save an object, but if I save another object, it will erase the previous item. I'm using gson lib to save my items. After some researches I've seen this How to use SharedPreferences to save more than one values?
But I can't use it because of my custom objects, if I use .toString(), I will not be able to get back my original item. I know that's it's the same key used to save object that will erase the previous one but I dont really know how to give a different key every time I will save an item.
Code to add :
addFav.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (currentProduit.getIsAdded() ==0) {
SharedPreferences.Editor prefsEditor = mPrefs.edit();
Gson gson = new Gson();
String myJson = gson.toJson(currentProduit);
Log.i("INFO", "Value of saved data" + myJson);
prefsEditor.putString("myproduct", myJson);
prefsEditor.apply();
Toast.makeText(getApplicationContext(), "Data saved !", Toast.LENGTH_SHORT).show();
addFav.setText(R.string.delete_fav);
currentProduit.setIsAdded(1);
} else {
addFav.setText(R.string.add_fav);
currentProduit.setIsAdded(0);
SharedPreferences.Editor editor = mPrefs.edit();
editor.remove("myproduct").apply();
Toast.makeText(getApplicationContext(), "Data removed !", Toast.LENGTH_SHORT).show();
}
}
});
Code to get back from other activity:
String myJson = mPrefs.getString("myproduct", "");
Log.i("INFO", "Value of loaded data" + myJson);
if (myJson.isEmpty() && favProductList.isEmpty()) {
listview_R.setAdapter(null);
Log.i("INFO", "No items");
title.setText(getString(R.string.fav));
} else if (myJson.isEmpty() && favProductList != null) {
myCustomAdapterVersionR = new CustomAdapter_VersionR(getApplicationContext(), favProductList);
listview_R.setAdapter(myCustomAdapterVersionR);
} else {
Product savedProduct = gson.fromJson(myJson, Product.class);
favProductList.add(savedProduct);
Log.i("INFO", "Favorite was added");
myCustomAdapterVersionR = new CustomAdapter_VersionR(getApplicationContext(), favProductList);
listview_R.setAdapter(myCustomAdapterVersionR);
}
Thanks for helping ! Btw, since it's not saving a lot of items, I didnt use sqlite db, cheers !
EDIT: I tried Juan Cortés solution, but I have this error after getting back the shared preferences --> error: incompatible types: CustomProduct[] cannot be converted to List, here is the code
if (fromPrefs.isEmpty() && favProductList.isEmpty()) {
listview_R.setAdapter(null);
Log.i("INFO", "No items");
title.setText(getString(R.string.fav));
} else {
//Product savedProduct = gson.fromJson(fromPrefs, Product.class);
//favProductList.add(savedProduct);
//Get the Object array back from the String `fromPrefs`
CustomProduct[] reInflated = gson.fromJson(fromPrefs,CustomProduct[].class);
Log.i("INFO", "Favorite was added");
myCustomAdapterVersionR = new CustomAdapter_VersionR(getApplicationContext(), reInflated); //error
listview_R.setAdapter(myCustomAdapterVersionR);
}
Thanks !
As an overly simplified app for example, you could define a custom class as follows (of course you'll have to adapt it to your particulars). The concept is create an array of custom objects, convert it to json, store it. It's really straightforward once you see it.
The code
Gson gson = new Gson();
//Create an array to work with it, dummy content
CustomProduct[] exampleList = new CustomProduct[10];
for(int i=0;i<10;i++){
exampleList[i] = new CustomProduct("string","number:"+i);
}
//Get a String representation of the objects
String forStoring = gson.toJson(exampleList);
//HERE you can store and retrieve to SharedPreferences
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.edit().putString("myarrayofcustomobjects", forStoring).commit();
//Get the string back from the SharedPreferences
String fromPrefs = prefs.getString("myarrayofcustomobjects","");
//Get the Object array back from the String `fromPrefs`
CustomProduct[] reInflated = gson.fromJson(fromPrefs,CustomProduct[].class);
Notes
If you already have a set of objects in an array, you'll need to inflate the array as shown above, create a new array with those elements + the one you want to add, convert them to a string again, and store them. Once this becomes too much of a hassle, you'll move to another means of persisting data for you app, but for as long as there are not that many, it should be ok.
Assuming
To get this to work, I'm assuming you have a Custom object named CustomProduct with the following definition:
public class CustomProduct {
String field1,field2;
public CustomProduct(String field1, String field2){
super();
this.field1 = field1;
this.field2 = field2;
}
#Override
public String toString() {
return "CustomProduct [field1="+field1+",field2="+field2+"]";
}
}
Update
User wants to show the results in a listview. You can define a custom adapter like the following to get it to work. Let this be the time for me to advise you to soon move towards RecyclerView instead of ListView but first tackle the problem you have, make it work, then improve upon it
public class CustomAdapter extends BaseAdapter{
private CustomProduct[] mProducts;
private LayoutInflater mInflater;
public CustomAdapter(Context context, CustomProduct[] products){
mProducts = products;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public int getCount() {
return mProducts.length;
}
public CustomProduct getItem(int i) {
return mProducts[i];
}
public long getItemId(int i) {
return i;
}
public View getView(int i, View convertView, ViewGroup parent) {
//Purposely not doing view recycling for sake of clarity
View row = mInflater.inflate(R.layout.custom_row,parent,false);
//Set the data from the row
((TextView)row.findViewById(R.id.field1)).setText(getItem(i).field1);
((TextView)row.findViewById(R.id.field2)).setText(getItem(i).field2);
//Return the view
return row;
}
}
By setting this adapter to your ListView and creating the layout (which simply consists in two textviews with the given ids) you will get the following result. You can try removing the part where it creates the data after it's run the first time and leaving only the part where it fetches the data to ensure it's persisted.
I'm new to Android Development and I'm trying to develop my first Android app which gets data from some public APIs using Android Volley.
I'm using singleton Volley Request Queue which is initialized in the launcher activity. I am successfully able to parse the JSON contents and display them on a Fragment layout/view (uses RecyclerView & CardView) when I set my RecyclerView adapters INSIDE the Volley's JsonObjectRequest.
The following code does display data, but suffers from time race condition.
Note: RvJoiner is a library which merges multiple adapters and makes a single adapter ordered by FIRST COME FIRST SERVE basis.
My Fragment class is as follows:
public class Fragment1 extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.recylcer_main, container, false);
ParseJSON parseJSON = new ParseJSON(v);
parseJSON.makeRequest1();
parseJSON.makeRequest2();
return v;
}
}
My ParseJSON class is as follows
public class ParseJSON {
private static final String URL1 = "some url";
private static final String URL2 = "some other url";
private static final String TAG = "ParseJSON";
private RequestQueue requestQueue;
private boolean FLAG_REQUEST1_FETCHED;
private boolean FLAG_REQUEST2_FETCHED;
private ArrayList<status1> status1ArrayList;
private ArrayList<status2> status2ArrayList;
private Context context;
private RvJoiner rvJoiner;
private View view;
ProgressDialog pd;
ParseJSON (View v){
this.view= v;
this.context=v.getContext();
pd = ProgressDialog.show(v.getContext(), "Please Wait", "Getting Data from APIs", true);
requestQueue = AppController.getInstance(v.getContext()).getRequestQueue();
rvJoiner = new RvJoiner();
}
public void makeRequest1() {
JsonObjectRequest request1 = new JsonObjectRequest(Request.Method.GET, URL1,
null, new Response.Listener<JSONObject>() {
#Override
public void onResponse(JSONObject response) {
try {
/* Parsing Stuff and storing it in status1ArrayList */
FLAG_REQUEST1_FETCHED=true;
Status1Adapter status1Adapter = new Status1Adapter(status1ArrayList);
RecyclerView recList = (RecyclerView) view.findViewById(R.id.cardList);
recList.setLayoutManager(new LinearLayoutManager(context));
rvJoiner.add(new JoinableAdapter(status1Adapter));
recList.setAdapter(rvJoiner.getAdapter());
pd.dismiss();
}
} catch (JSONException e) {}
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {}
});
AppController.getInstance(context).addToRequestQueue(request1);
}
public void makeRequest2() {
JsonObjectRequest request2 = new JsonObjectRequest(Request.Method.GET, URL2,
null, new Response.Listener<JSONObject>() {
#Override
public void onResponse(JSONObject response) {
try {
/* Parsing stuff and storing it inside ArrayList status2ArrayList */
FLAG_REQUEST2_FETCHED=true;
Status2Adapter status2Adapter = new Staus2Adapter(status2ArrayList);
RecyclerView recList = (RecyclerView) view.findViewById(R.id.cardList);
recList.setLayoutManager(new LinearLayoutManager(context));
rvJoiner.add(new JoinableAdapter(status2Adapter));
}
} catch (JSONException e) {}
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {}
});
AppController.getInstance(context).addToRequestQueue(request2);
}
public boolean isStatusFetched(){
return FLAG_REQUEST1_FETCHED && FLAG_REQUEST2_FETCHED;
}
public ArrayList<status1> getstatus1ArrayList() {
return status1ArrayList;
}
public ArrayList<status2> getstatus2ArrayList() {
return status2ArrayList;
}
}
In the above code, I'm having a race condition. Since Volley network calls are asynchronous in nature, I have no control on which request will get completed and displayed on my Fragment CardView first. (i.e any of rvJoiner.add() requests can be executed first)
I would like to make my UI consistent i.e I want Request1 adapter to be added to RvJoiner first and then the Request2.
If possible, I would like to move all my code that sets adapters and joins them from JsonObjectRequest to my Fragment's onCreateView method. So, in this way, I have a control on the order of adapters. However, then I need a method which checks the value of FLAG_REQUEST1_FETCHED and FLAG_REQUEST2_FETCHED via isStatusFetched method continuously.
Code for the Fragment class will be
public class Fragment1 extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.recylcer_main, container, false);
ParseJSON parseJSON = new ParseJSON(v);
parseJSON.makeRequest1();
parseJSON.makeRequest2();
while(!parseJSON.isDataFetched()){
/* I want to wait here till both status1ArrayList and status2ArrayList gets populated with data in ParseJSON. In this way I can control the order in which adapters are added inside RvJoiner. If I don't wait here I will get NullPointerException on runtime since Volley calls are asynchronous and getStatus1ArrayList/getStatus2ArrayList will most probably return null. But how to wait here without consuming too much CPU power? */
}
ArrayList<status1> status1ArrayList = parseJSON.getstatus1ArrayList();
ArrayList<status2> status2ArrayList = parseJSON.getstatus2ArrayList();
Status1Adapter status1Adapter = new Status1Adapter(status1ArrayList);
Status2Adapter status2Adapter = new Status2Adapter(status2ArrayList);
RecyclerView recList = (RecyclerView) v.findViewById(R.id.cardList);
recList.setLayoutManager(new LinearLayoutManager(v.getContext()));
RvJoiner rvJoiner = new RvJoiner();
/* Problem solved as I'm adding adapters in the order I want */
rvJoiner.add(new JoinableAdapter(status1Adapter));
rvJoiner.add(new JoinableAdapter(status2Adapter));
recList.setAdapter(rvJoiner.getAdapter());
return v;
}
}
One solution can be using callbacks. I read somewhere about them, but I'm not sure if it solves my problem of 'multiple request at the same time while maintaining order'.
Another solution would be to restrict my Volley Queue to handle one request at one time only but that would increase the time taken to fetch and serve data. This is my last choice.
I am virtually out of ideas and would like someone to help me so that I can control the order of setting my adapters and maintain a consistent UI. If you need any other information, please tell me.
Thanks.
This is how avoiding race conditions for two requests work in general. You should work with callbacks. The implementation of your onResponse methods are callbacks because those methods are called after one request is done. Response handling works on the UI thread right ? So the responses can just be handled one by the other.
This means you just have to maintain order there. Extract the work you would like to do after getting one response. You need some boolean flags indicating whether your requests are done. Pseudocode would look like this:
request1Done = false;
request2Done = false;
doRequest1();
doRequest2();
onResponse1() {
doWorkForRequest1(); // always start handling the response
request1Done = true;
if (request2Done) { // if this is true, request2 was faster than request1
doWorkForRequest2();
}
};
onResponse2() {
request2Done = true;
if (request1Done) { // request1 did its work, no its request2's turn
doWorkForRequest2();
}
};
So basically you should fix your onReponse methods. Hope this will help you. :)
I was thinking. What is the best way to save a custom adapter before activity onDestroy() is called? I want to populate an adapter with items (texts and images) and set it to listView. However, I don't want to repopulate the adapter again when the user navigate away from that activity and comes back as repopulation is too time consuming. I want to save the adapter value somewhere before the activity inDestroy() is called and check if it empty on activity onCreate.
Well, adapter is a pretty complex object and its persistent saving may be a difficult task (if possible at all). The more common approach is saving persistently your dataset.
You worry about the population time, but serialization-deserialization of the adapter is going to take time as well, and apparently much more time then the dataset alone, because it includes the dataset in it.
EDIT
Small conceptual example on saving your dataset to SharedPreferences using Gson library (more on it) (just one of the ways to persistently save your data):
public void saveData(ArrayList<YourDataType> data) {
Gson gson = new Gson();
String dataJson = gson.toGson(data);
getSharedPreferences("your_prefs", Context.MODE_PRIVATE).edit()
.putString("key_data", dataJson)
.apply();
}
public ArrayList<YourDataType> restoreData() {
String dataJson =
getSharedPreferences("your_prefs", Context.MODE_PRIVATE)
.getString("key_data", "empty");
ArrayList<YourDataType> data = null;
if (!dataJson.equals("empty")) {
Gson gson = new Gson();
Type collectionType = new TypeToken<ArrayList<YourDataType>>() {}.getType();
data = gson.fromJson(dataJson, collectionType);
}
return data;
}
You can save adapter data inside application class and when activity is recreated check Application class arraylist is empty or not. If not empty
assign it to adapter.
But storing it inside global variable i.e Application class may make your app heavy on heap memory.
public class GlobalState extends Application {
ArraList<Type> arrayList = new ArrayList<type>();
public void setArraylist(ArraList<Type> arrayList) {
this.arrayList = arrayList;
}
public ArraList<Type> getArrayList() {
return arrayList;
}
public int dataSize() {
return arrayList.size();
}
}