I'm using a third party cloud service (Kinvey) to fetch data for my app. All Kinvey methods that service are wrapped in async classes. I'm currently getting an error log below and I understand why I am getting it. I just don't know how to resolve the problem.
Error :
03-12 13:41:03.449: E/AndroidRuntime(18375): FATAL EXCEPTION: main
03-12 13:41:03.449: E/AndroidRuntime(18375): java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(2130968661, class android.widget.ListView) with Adapter(class com.example.chartviewer.LazyAdapter)]
Here is the segment of my code where I notify my adapter after deleting content within an async method :
mKinveyClient.appData("artistdata", ArtistData.class).delete(artistID, new KinveyDeleteCallback() {
#Override
public void onSuccess(KinveyDeleteResponse result) {
Log.e("DELEET", "Data Deleted sucessfully");
Toast.makeText(getBaseContext(),
"Artist Deleted", Toast.LENGTH_SHORT).show();
//refreshes list
adapter.notifyDataSetChanged();
//displays dialog box if no artists left
displayNoArtist();
}
#Override
public void onFailure(Throwable error) {
Log.e("DELETE", "AppData.delete Failure", error);
}
});
Updating the adapter :
mKinveyClient.appData("artistdata", ArtistData.class).get(myQuery, new
KinveyListCallback<ArtistData>() {
#Override
public void onSuccess(ArtistData[] result) {
Log.e("FETCHING", "Data fetched sucessfully");
for (ArtistData data : result) {
String name= data.getArtistname();
String imageurl = data.getArtisturl();
String id = data.getArtistid();
artistIds.add(id);
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put(TAG_NAME, name);
hashMap.put(TAG_IMAGEURL, imageurl);
addMyArtists(hashMap);
}
displayData();
}
#Override
public void onFailure(Throwable error) {
Log.e("FETCHING", "AppData.get by Query Failure", error);
}
});
Adaptor creator code :
list = (ListView)findViewById(R.id.artistlist);
adapter = new LazyAdapter(MyArtistsActivity.this,"artists", artistItemList);
list.setAdapter(adapter);
registerForContextMenu(list);
Disclaimer: I am a member of Kinvey's engineering team.
The issue is that you are deleting from the artistItemList prior to deleting from the Kinvey backend, and then not calling adapter.notifyDatasetChanged until the callback. This results in the lag you noticed where the underlying dataset has changed while waiting for the callback from the backend Kinvey delete call. You need to group both of these calls together, and do it in one of two ways:
Make the artistID that you pass to the delete method final, and in onSuccess, remove the item from artistItemList before calling adapter.notifyDatasetChanged.
Call adapter.notifyDatasetChanged before calling the kinveyClient.appData().delete() method, immediately after removing the item from artistItemList. This option may cause issues if the delete fails on the Kinvey back-end.
Related
When I manually add the objects for the data of the recycler view in the required ArrayList, it works fine, but as soon as I fetch the data from my Node JS MongoDB backend, it shows nothing.
The Java part:
Call<ArrayList<BookingDetails>> call = retrofitInterface.executeAdminPendingReq();
call.enqueue(new Callback<ArrayList<BookingDetails>>() {
#Override
public void onResponse(Call<ArrayList<BookingDetails>> call, Response<ArrayList<BookingDetails>> response) {
if(response.code() == 200){
bookingDetails = response.body();
Log.i("Something", bookingDetails.get(0).getPurpose());
Toast.makeText(PendingRequestActivity.this, "Something to show", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(PendingRequestActivity.this, "Nothing to show", Toast.LENGTH_SHORT).show();
}
}
#Override
public void onFailure(Call<ArrayList<BookingDetails>> call, Throwable t) {
Toast.makeText(PendingRequestActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
The Node JS part:
app.post('/admin_pendingReq', async(req, res) => {
const cursor = bookingReqCollection.find({}, {projection: {_id: 0, confirm: 0}})
const results = await cursor.toArray()
if(results.length > 0){
res.status(200).send(JSON.stringify(results))
}else{
res.status(400).send()
}
})
Also, one thing to note is if I try to log the data fetched from Node in Android Studio (which is present above), it shows the required data.
EDIT: I solved the issue by setting the recycler view adapter and the layout manager inside of the onResponse method after taking it out from the onCreate method. But I would still like to know why this has happened
I have multiple buttons in my android application and when clicked it has to fetch data from Firebase database and the fetched data is added into a list which has to be passed as an argument to another class constructor . But the fetching of data happens in another thread by default since firebase queries are asynchronous . How do I make sure that list has been added with data from other thread before passing it to constructor ?
It's simple use oncompletelistener you can refer this example:
db=FirebaseFirestore.getInstance();
db.collection("Order").document(TableListFragment.tableno)
.update(
"Items", FieldValue.arrayUnion("ABCD"),
"Quantity", FieldValue.arrayUnion("34")
).addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
Toast.makeText(getContext(),"Item Added",Toast.LENGTH_LONG).show();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.i("onFailure:",e.toString());
}
});
I am fetching JSON array of user ids from a server (not Firebase server).
I also store images of each user in Firebase storage. I have a dataset of Users, that contain user id and user image url. The JSON response is constantly updating, so every call I receive new response from the server with new list of user ids. The only solution I came up with, is to:
Clear dataset > Loop through the JSON Array to add all users to the empty dataset > notify dataset changed.
The problem with this is that it's not efficient: I notify data set changed on each iteration, and also since I clear the dataset every new response (from the remote server), the list refreshes, instead of simply adding / removing the necessary users.
This is how the code looks:
#Override
public void onResponse(JSONArray response) { // the JSON ARRAY response of user ids ["uid1", "uid334", "uid1123"]
myDataset.clear(); // clear dataset to prevent duplicates
for (int i = 0; i < response.length(); i++) {
try {
String userKey = response.get(i).toString(); // the currently iterated user id
final DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
DatabaseReference userKeyRef = rootRef.child("users").child(userKey); // reference to currently iterated user
ValueEventListener listener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
myDataset.add(new User(dataSnapshot.getKey(), dataSnapshot.child("imageUrl").getValue().toString())); //add new user: id and image url
mAdapter.notifyDataSetChanged(); // notify data set changed after adding each user (Not very efficient, huh?)
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
Log.d(TAG, databaseError.getMessage());
}
};
userKeyRef.addListenerForSingleValueEvent(listener);
}
catch (JSONException e) { Log.d(TAG, "message " + e); }
}
You might be interested in DiffUtil.
It uses an efficient algorithm to calculate the difference between your lists. And the cherry on the top is that this can be run on a background thread.
It is an alternative to notifyDataSetChanged() and is sort of an industry standard way for updating your RecyclerView
You can use Firebase Cloud functions.Pass the JSON Array to cloud function and retrieve the updated dataset in one go and notify the recycle view Here is link
I am using volley package to retrieve data from a website(JSON).
Here is my method
private void getEarthquakeList(){
// ...
// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
//Earthquake Feeder
String url ="https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/significant_month.geojson";
// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
#Override
public void onResponse(String response) {
Log.d("Response is: ",response);
//Parsing Json
parseJson(response);
final ListView earthquakeListView = (ListView)findViewById(R.id.list);
//Sort the array according to magnitude
earthquakeArrayList.sort((a, b) -> Double.compare(b.getTime(), a.getTime()));
mAdapter = new EarthquakeAdapter(getApplicationContext(), earthquakeArrayList);
earthquakeListView.setAdapter(mAdapter);
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
Log.d("Error",error.getMessage());
}
});
// Add the request to the RequestQueue.
queue.add(stringRequest);
}
The problem is that right now I am updating the UI inside this method after it returns the response.
These are the lines
//Sort the array according to a date
earthquakeArrayList.sort((a, b) -> Double.compare(b.getTime(), a.getTime()));
mAdapter = new EarthquakeAdapter(getApplicationContext(), earthquakeArrayList);
earthquakeListView.setAdapter(mAdapter);
While I am running in the MainActivity there is no issue, the user opens the app and gets the list of earthquakes.
The issues start when I want to switch to service where I monitor every couple of minutes or when the website content is changing.
So I want my method without updating the UI in it.
The problem is that If I am not updating the UI inside onResponse, the UI thread continues and results in an empty array.
So this array stay empty earthquakeArrayList if I am not doing it inside
public void onResponse(String response)
Ideas how to separate the two, in one hand running the method and fetching the data and on the other hand the main thread will be able to access the data and not finish executing.
Thanks
EG
The recommended solution right now is to use LiveData. LiveData works on the Observer Pattern, so you can have any number of observers to the updated data both in the Service and the Activity but they need to be under the same context. These are a bit advanced topics but a good way to decouple your UI with the data layer. You can go through these two links
Google IO 2018 session on Livedata
Medium Article on LiveData
What I want to achieve is to be able to observe changes in my web service and then update my textview if there is any change. I am currently using timer to achieve this by running it every x second. The problem though, is that the memory leaks so it's not a good solution. Now I stumbled upon this rxjava/rxjava but I'm confused on how to use it. The documentation is confusing to me and I can't find alot of tutorials about this. I am using volley to get data from my web service by the way.
this is the Observable that someone on answered on my other question but I'm getting an error which is "Incompatible types" on return sendRequest.
Observable.interval(500, TimeUnit.MILLISECONDS, Schedulers.io()).map(new Func1<Long, Object>() {
#Override
public Object call(Long tick) {
return sendRequest();
}
}).observeOn(AndroidSchedulers.mainThread()).subscribe();
here is my volley request code
public void sendRequest(){
//While the app fetched data we are displaying a progress dialog
//final ProgressDialog loading = ProgressDialog.show(this,"Fetching Data","Please wait...",false,false);
StringRequest stringRequest = new StringRequest(JSON_URL,
new Response.Listener<String>() {
#Override
public void onResponse(String response) {
//text.setText(response);
//loading.dismiss();
showJSON(response);
}
},
new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
//Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_LONG).show();
}
});
RequestQueue requestQueue = Volley.newRequestQueue(MainActivity.this);
requestQueue.add(stringRequest);
}
private void showJSON(String json){
ParseJson pj = new ParseJson(json);
pj.parseJSON();
text.setText(ParseJson.playing[0]);
}
Your method sendRequest doesn't return anything(void). You can either return null or something.
#Override
public Object call(Long tick) {
sendRequest();
return null;
}
I would suggest you firstly read Java basics instead of writing Android app.
Use push notifications to send messages from the server to the users when data is updated and avoid sending unwanted requests to the server.
Then you can send the request for new data only when notified and update the Observer someway, maybe use a rx subject, or better store the data in SQLite table and observe changes from the DB.
Recommend this to create a rx stream from sqlite
https://github.com/square/sqlbrite
GCM: https://developers.google.com/cloud-messaging/