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. :)
Related
I want to show a list of information about devices. First, I call API to get a list of devices. After that, in onResponse function, I for each device and call another API to get data of each device. But this data is not updated in View. View just show a list of device, can not show data of each device. I think that observe don't catch event when updating data of devices
My Device class:
public class Device {
#SerializedName("Altitude")
private Double altitude;
#SerializedName("Latitude")
private Double latitude;
#SerializedName("Longtitude")
private Double longitude;
#SerializedName("NodeId")
private String nodeId;
#SerializedName("ReverseGeocode")
private String reverseGeocode;
#SerializedName("Title")
private Integer title;
#Expose
private List<Data> data = new ArrayList<>();
}
My repository:
public class DeviceRepository {
DeviceApi deviceApi;
MutableLiveData<List<Device>> mutableLiveData = new MutableLiveData<>();
//some code to init...
//get devices
public MutableLiveData<List<Device>> getDevices() {
final List<Device> devices = new ArrayList<>();
Call<List<Device>> call = deviceApi.getDevices();
call.enqueue(new Callback<List<Device>>() {
#Override
public void onResponse(Call<List<Device>> call, Response<List<Device>> response) {
if(response.body() != null) {
for (Device device: response.body()) {
devices.add(device);
}
mutableLiveData.setValue(devices);
//set data for device by call API
for(int i = 0; i<mutableLiveData.getValue().size(); i++){
DeviceRepository.this.getDataOfDevice(mutableLiveData.getValue().get(i));
}
}
}
#Override
public void onFailure(Call<List<Device>> call, Throwable t) {
}
});
return mutableLiveData;
}
//call API to get Data
public void getDataOfDevice(Device device) {
final List<Data> data = new ArrayList<>();
Call<List<Data>> call = deviceApi.getDataByNodeId(device.getNodeId());
call.enqueue(new Callback<List<Data>>() {
#Override
public void onResponse(Call<List<Data>> call, Response<List<Data>> response) {
if(response.body() != null && response.body().size()>0) {
data.add(response.body().get(0));
device.setData(data);
}
}
#Override
public void onFailure(Call<List<Data>> call, Throwable t) {
System.out.println("fail");
}
});
}
}
This is my ViewModel:
public class DeviceViewModel extends ViewModel {
private MutableLiveData<List<Device>> devices;
private DeviceRepository deviceRepository = new DeviceRepository();
public MutableLiveData<List<Device>> getDevices() {
devices = deviceRepository.getDevices();
return devices;
}
}
This is Fragment:
public class DeviceFragment extends Fragment{
private DeviceViewModel deviceViewModel;
public View onCreateView(#NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
DeviceFragmentBinding binding = DataBindingUtil.inflate(inflater, R.layout.device_fragment, container, false);
binding.setLifecycleOwner(this);
View root = inflater.inflate(R.layout.device_fragment, container, false);
deviceViewModel = new ViewModelProvider(this).get(DeviceViewModel.class);
final ListView listView = root.findViewById(R.id.list);
deviceViewModel.getDevices().observe(getViewLifecycleOwner(), devices -> {
final List<String> list = new ArrayList<>();
for (Device device: devices) {
String info = device.getData().size() > 0 ? device.getData().get(0).getTime().toString() : "no data";
list.add(device.getNodeId() + "-"+info);
}
ArrayAdapter<String> itemsAdapter =
new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, list);
listView.setAdapter(itemsAdapter);
});
return root;
}
}
And device_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.Device.DeviceFragment">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/list">
</ListView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Your problem is with LiveData managing and about async operations
All Retrofit "enqueue" are asynchronous, each result you can catch in callback - onResponse.
Let's say your first response you will get after 1 second, then you send another 10 request for devices details, and you wait for response (first of them will come in 1.5 second, second - in 1.2, third - in 2 and so on). So to get whole data about devices you should wall until all request will be fulfilled. And only then to send with LiveData mark to UI, that data is ready and it could be updated.
You observe LiveData in your Fragment. But when it's callback invoked (that place where you set data in your adapter)? Answer - just after you set your LiveData's value in your Repository. And you do it right after getting first request (not waiting for return rest of request about devices' data). That's why you don't get detailed info about devices' data in your fragment.
// 1. your first request done (let's say after 1 second)
// 2. you set you LiveData value, and update your UI, but other request are not ready
mutableLiveData.setValue(devices);
// 3. rest of your requests done (let's say after 3 seconds)
// <- there you should set your LiveData
If you use Java, you can wait for all request chaining them - you can do it with RxJava. There are lot articles on StackOverflow about that. Here is one of them.
I got a response back from the API like this below:
{
"transactions": [null]
}
However, when I tried to debug, List.getTransactionItems().size() is equal 1 rather than 0. I think it considers null as an item. Also, I checked few things as below but none of them work.
if (this.transactionsViewModel.getTransactionItems().size() == 0
|| this.transactionsViewModel.getTransactionItems() == null
|| this.transactionsViewModel.getTransactionItems().isEmpty()
|| this.transactionsViewModel.getTransactionItems().equals(null))
However, when I tried to call something like that below, it actually recognized that there is an null item in the list.
this.transactionsViewModel.getTransactionItems().contains(null)
Any idea in this situation?
Thanks in advance.
As far as i know, json array not supposed to be null like that. You can make "transactions": [] or "transactions": null instead. That behaviour happens because your code recognise that the array is not empty. It has 1 item with null value.
If you can not change the server response.
You can check if the list only contains null by removeAll null object from your list then check the size
(this.transactionsViewModel.getTransactionItems().removeAll(Collections.singleton(null))).size() == 0
// size == 0 your list only contains null
// size > 0 your list is not empty
How about reading the stream into a string, and doing a search and replace on the json string, and remove the null, before you parse the json? You can also extend a stream class, and do it in the stream.
This is the problem about the unsuitable response from the server. To solve your problem, you can only to make some workaround in your code.
I suggest you should keep your application as clear and origin as possible. Don't change the condition call if it is really necessary.
The following is my suggested code ( used some code from Phan Van Linh)
This class used Retrofit library as the example to make the callback abstract
public class TestFragment extends BaseFragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
view = inflater.inflate(R.layout.testlayout, null, false);
RestA rest = UtilRetrofit.build(RestA.class);//from the restful endpoint
Call<YourObject> call = rest.getTransactions();
call.enqueue(new TransactionCallback() {
#Override
public void onResponse(YourObject bean) {
Toast.makeText(getActivity(), "Hello World", Toast.LENGTH_SHORT).show();
}
#Override
public void onFailure(Call<YourObject> call, Throwable t) {
}
});
return view;
}
public abstract class TransactionCallback implements Callback<YourObject> {
#Override
public void onResponse(Call<YourObject> call, Response<YourObject> response) {
YourObject lNewBean = response.body();
lNewBean.getTransactions().removeAll(Collections.singleton(null));// do some custom action and remove useless thing
onResponse(lNewBean);
}
public abstract void onResponse(YourObject bean);
}
/**
* Created by me on 22/8/2016.
*/
public static class YourObject {
private List<String> transactions;
public List<String> getTransactions() {
return transactions;
}
public void setTransactions(List<String> pTransactions) {
transactions = pTransactions;
}
}
}
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.
I am using a singleton for fetching data from a web service and storing the resulting data object in an ArrayList. It looks like this:
public class DataHelper {
private static DataHelper instance = null;
private List<CustomClass> data = null;
protected DataHelper() {
data = new ArrayList<>();
}
public synchronized static DataHelper getInstance() {
if(instance == null) {
instance = new DataHelper();
}
return instance;
}
public void fetchData(){
BackendlessDataQuery query = new BackendlessDataQuery();
QueryOptions options = new QueryOptions();
options.setSortBy(Arrays.asList("street"));
query.setQueryOptions(options);
CustomClass.findAsync(query, new AsyncCallback<BackendlessCollection<CustomClass>>() {
#Override
public void handleResponse(BackendlessCollection<CustomClass> response) {
int size = response.getCurrentPage().size();
if (size > 0) {
addData(response.getData());
response.nextPage(this);
} else {
EventBus.getDefault().post(new FetchedDataEvent(data));
}
}
#Override
public void handleFault(BackendlessFault fault) {
EventBus.getDefault().post(new BackendlessFaultEvent(fault));
}
});
}
public List<CustomClass> getData(){
return this.data;
}
public void setData(List<CustomClass> data){
this.data = data;
}
public void addData(List<Poster> data){
this.data.addAll(data);
}
public List<CustomClass> getData(FilterEnum filter){
if(filter == FilterEnum.NOFILTER){
return getData();
}else{
// Filtering and returning filtered data
}
return getData();
}
}
The data is fetched correctly and the list actually contains data after it. Also, only one instance is created, as intended. However, whenever I call getData later, the length of this.data is 0. Because of this I also tried it with a subclass of Application holding the DataHelper object, resulting in the same problem.
Is there a good way of debugging this? Is there something like global watches in Android Studio?
Is there something wrong with my approach? Is there a better approach? I am mainly an iOS developer, so Android is pretty new to me. I am showing the data from the ArrayList in different views, thus I want to have it present in an the ArrayList as long as the application runs.
Thanks!
EDIT: Example use in a list view fragment (only relevant parts):
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
filter = FilterEnum.NOFILTER;
data = DataHelper.getInstance().getData(filter);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
customClassListAdapter = new customClassListAdapter(getActivity(), data);}
EDIT2: Added code where I fetch the data from Backendless, changed reference of DataHelper to reference of data in first EDIT
EDIT3: I usa a local EventBus for notifying the list view about the new data. This looks like this and works (initially the data gets populated, but after e.g. applying a filter, the ArrayList I get with getData is empty):
#Subscribe
public void onMessageEvent(FetchedDataEvent event) {
customClassListAdapter.notifyDataSetChanged();
}
Try instead of keeping reference to your DataHelper instance, keeping reference to your list of retrieved items. F.e. when you first fetch the list (and it's ok as you say), assign it to a class member. Or itarate through it and create your own array list of objects for future use.
Okay I finally found the problem. It was not about the object or memory management at all. Since I give the reference on getData to my ArrayAdapter, whenever I call clear (which I do when changing the filter) on the ArrayAdapter, it empties the reference. I basically had to create a copy of the result for the ArrayAdapter:
data = new ArrayList<>(DataHelper.getInstance().getData(filter));
I was not aware of the fact that this is a reference at all. So with this the data always stays in the helper entirely. I only did this because this:
customClassListAdapter.notifyDataSetChanged();
does hot help here, it does not call getData with the new filter again.
Thanks everyone for your contributions, you definitely helped me to debug this.
It is likely that getData does get called before the data is filled.
A simple way to debug this is to add (import android.util.Log) Log.i("MyApp.MyClass.MyMethod", "I am here now"); entries to strategic places in fetchData, addData and getData and then, from the logs displayed by adb logcat ensure the data is filled before getData gets called.
I am getting data from server and then parsing it and storing it in a List. I am using this list for the RecyclerView's adapter. I am using Fragments.
I am using a Nexus 5 with KitKat. I am using support library for this. Will this make a difference?
Here is my code: (Using dummy data for the question)
Member Variables:
List<Business> mBusinesses = new ArrayList<Business>();
RecyclerView recyclerView;
RecyclerView.LayoutManager mLayoutManager;
BusinessAdapter mBusinessAdapter;
My onCreateView():
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Getting data from server
getBusinessesDataFromServer();
View view = inflater.inflate(R.layout.fragment_business_list,
container, false);
recyclerView = (RecyclerView) view
.findViewById(R.id.business_recycler_view);
recyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(mLayoutManager);
mBusinessAdapter = new BusinessAdapter(mBusinesses);
recyclerView.setAdapter(mBusinessAdapter);
return view;
}
After getting data from server, parseResponse() is called.
protected void parseResponse(JSONArray response, String url) {
// insert dummy data for demo
mBusinesses.clear();
Business business;
business = new Business();
business.setName("Google");
business.setDescription("Google HeadQuaters");
mBusinesses.add(business);
business = new Business();
business.setName("Yahoo");
business.setDescription("Yahoo HeadQuaters");
mBusinesses.add(business);
business = new Business();
business.setName("Microsoft");
business.setDescription("Microsoft HeadQuaters");
mBusinesses.add(business);
Log.d(Const.DEBUG, "Dummy Data Inserted\nBusinesses Length: "
+ mBusinesses.size());
mBusinessAdapter = new BusinessAdapter(mBusinesses);
mBusinessAdapter.notifyDataSetChanged();
}
My BusinessAdapter:
public class BusinessAdapter extends
RecyclerView.Adapter<BusinessAdapter.ViewHolder> {
private List<Business> mBusinesses = new ArrayList<Business>();
// Provide a reference to the type of views that you are using
// (custom viewholder)
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTextViewName;
public TextView mTextViewDescription;
public ImageView mImageViewLogo;
public ViewHolder(View v) {
super(v);
mTextViewName = (TextView) v
.findViewById(R.id.textView_company_name);
mTextViewDescription = (TextView) v
.findViewById(R.id.textView_company_description);
mImageViewLogo = (ImageView) v
.findViewById(R.id.imageView_company_logo);
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public BusinessAdapter(List<Business> myBusinesses) {
Log.d(Const.DEBUG, "BusinessAdapter -> constructor");
mBusinesses = myBusinesses;
}
// Create new views (invoked by the layout manager)
#Override
public BusinessAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
Log.d(Const.DEBUG, "BusinessAdapter -> onCreateViewHolder()");
// create a new view
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.item_business_list, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
Log.d(Const.DEBUG, "BusinessAdapter -> onBindViewHolder()");
Business item = mBusinesses.get(position);
holder.mTextViewName.setText(item.getName());
holder.mTextViewDescription.setText(item.getDescription());
holder.mImageViewLogo.setImageResource(R.drawable.ic_launcher);
}
// Return the size of your dataset (invoked by the layout manager)
#Override
public int getItemCount() {
Log.d(Const.DEBUG, "BusinessAdapter -> getItemCount()");
if (mBusinesses != null) {
Log.d(Const.DEBUG, "mBusinesses Count: " + mBusinesses.size());
return mBusinesses.size();
}
return 0;
}
}
But I don't get the data displayed in the view. What am I doing wrong?
Here is my log,
07-14 21:15:35.669: D/xxx(2259): Dummy Data Inserted
07-14 21:15:35.669: D/xxx(2259): Businesses Length: 3
07-14 21:26:26.969: D/xxx(2732): BusinessAdapter -> constructor
I don't get any logs after this. Shouldn't getItemCount() in adapter should be called again?
In your parseResponse() you are creating a new instance of the BusinessAdapter class, but you aren't actually using it anywhere, so your RecyclerView doesn't know the new instance exists.
You either need to:
Call recyclerView.setAdapter(mBusinessAdapter) again to update the RecyclerView's adapter reference to point to your new one
Or just remove mBusinessAdapter = new BusinessAdapter(mBusinesses); to continue using the existing adapter. Since you haven't changed the mBusinesses reference, the adapter will still use that array list and should update correctly when you call notifyDataSetChanged().
Try this method:
List<Business> mBusinesses2 = mBusinesses;
mBusinesses.clear();
mBusinesses.addAll(mBusinesses2);
//and do the notification
a little time consuming, but it should work.
Just to complement the other answers as I don't think anyone mentioned this here: notifyDataSetChanged() should be executed on the main thread (other notify<Something> methods of RecyclerView.Adapter as well, of course)
From what I gather, since you have the parsing procedures and the call to notifyDataSetChanged() in the same block, either you're calling it from a worker thread, or you're doing JSON parsing on main thread (which is also a no-no as I'm sure you know). So the proper way would be:
protected void parseResponse(JSONArray response, String url) {
// insert dummy data for demo
// <yadda yadda yadda>
mBusinessAdapter = new BusinessAdapter(mBusinesses);
// or just use recyclerView.post() or [Fragment]getView().post()
// instead, but make sure views haven't been destroyed while you were
// parsing
new Handler(Looper.getMainLooper()).post(new Runnable() {
public void run() {
mBusinessAdapter.notifyDataSetChanged();
}
});
}
PS Weird thing is, I don't think you get any indications about the main thread thing from either IDE or run-time logs. This is just from my personal observations: if I do call notifyDataSetChanged() from a worker thread, I don't get the obligatory Only the original thread that created a view hierarchy can touch its views message or anything like that - it just fails silently (and in my case one off-main-thread call can even prevent succeeding main-thread calls from functioning properly, probably because of some kind of race condition)
Moreover, neither the RecyclerView.Adapter api reference nor the relevant official dev guide explicitly mention the main thread requirement at the moment (the moment is 2017) and none of the Android Studio lint inspection rules seem to concern this issue either.
But, here is an explanation of this by the author himself
I had same problem. I just solved it with declaring adapter public before onCreate of class.
PostAdapter postAdapter;
after that
postAdapter = new PostAdapter(getActivity(), posts);
recList.setAdapter(postAdapter);
at last I have called:
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
// Display the size of your ArrayList
Log.i("TAG", "Size : " + posts.size());
progressBar.setVisibility(View.GONE);
postAdapter.notifyDataSetChanged();
}
May this will helps you.
Although it is a bit strange, but the notifyDataSetChanged does not really work without setting new values to adapter. So, you should do:
array = getNewItems();
((MyAdapter) mAdapter).setValues(array); // pass the new list to adapter !!!
mAdapter.notifyDataSetChanged();
This has worked for me.
Clear your old viewmodel and set the new data to the adapter and call notifyDataSetChanged()
In my case, force run #notifyDataSetChanged in main ui thread will fix
public void refresh() {
clearSelection();
// notifyDataSetChanged must run in main ui thread, if run in not ui thread, it will not update until manually scroll recyclerview
((Activity) ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
adapter.notifyDataSetChanged();
}
});
}
I always have this problem that I forget that the RecyclerView expects a new instance of a List each time you feed the adapter.
List<X> deReferenced = new ArrayList(myList);
adapter.submitList(deReferenced);
Having the "same" List (reference) means not declaring "new" even if the List size changes, because the changes performed to the List also propagates to other Lists (when they are simply declared as this.localOtherList = myList) emphasis on the keyword being "=", usually components that compare collections make a copy of the result after the fact and store it as "old", but not Android DiffUtil.
So, if a component of yours is giving the same List each and every time you submit it, the RecyclerView won't trigger a new layout pass.
The reason is that... AFAIR, before the DiffUtil even attempts to apply the Mayers algorithm, there is a line doing a:
if (newList == mList)) {return;}
I am not sure how much "good practice" does de-referencing within the same system is actually defined as "good" ...
Specially since a diff algorithm is expected to have a new(revised) vs old(original) component which SHOULD in theory dereference the collection by itself after the process has ended but... who knows...?
But wait, there is more...
doing new ArrayList() dereferences the List, BUT for some reason Oracle decided that they should make a second "ArrayList" with the same name but a different functionality.
This ArrayList is within the Arrays class.
/**
* Returns a fixed-size list backed by the specified array. (Changes to
* the returned list "write through" to the array.) This method acts
* as bridge between array-based and collection-based APIs, in
* combination with {#link Collection#toArray}. The returned list is
* serializable and implements {#link RandomAccess}.
*
* <p>This method also provides a convenient way to create a fixed-size
* list initialized to contain several elements:
* <pre>
* List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
* </pre>
*
* #param <T> the class of the objects in the array
* #param a the array by which the list will be backed
* #return a list view of the specified array
*/
#SafeVarargs
#SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a); //Here
}
This write-through is funny because if you:
Integer[] localInts = new Integer[]{1, 2, 8};
Consumer<List<Integer>> intObserver;
public void getInts(Consumer<List<Integer>> intObserver) {
this.intObserver = intObserver;
dispatch();
}
private void dispatch() {
List<Integer> myIntegers = Arrays.asList(localInts);
intObserver.accept(myIntegers);
}
... later:
getInts(
myInts -> {
adapter.submitList(myInts); //myInts = [1, 2, 8]
}
);
Not only does the List dispatched obeys the dereferencing on each submission, but when the localInts variable is altered,
public void set(int index, Integer value) {
localInts[index] = value;
dispatch(); // dispatch again
}
...
myModel.set(1, 4) // localInts = [1, 4, 8]
this alteration is also passed to the List WITHIN the RecyclerView, this means that on the next submission, the (newList == mList) will return "false" allowing the DiffUtils to trigger the Mayers algorithm, BUT the areContentsTheSame(#NonNull T oldItem, #NonNull T newItem) callback from the ItemCallback<T> interface will throw a "true" when reaching index 1. basically, saying "the index 1 inside RecyclerView (that was supposed to be 2 in th previous version) was always 4", and a layout pass will still not perform.
So, the way to go in this case is:
List<Integer> trulyDereferenced = new ArrayList<>(Arrays.asList(localInts));
adapter.submitList(trulyDereferenced);