My RecyclerView crashes with an IndexOutOfBoundsException while scrolling after attempting to refresh the data.
Desired functionality: After API request has successfully populated RecyclerView once, I'd like to refresh the RecyclerView and be able to scroll up and down while it refreshes.
Current functionality: If I don't scroll while refreshing the data, the app doesn't crash. If I scroll after making a refresh request, it crashes with an IndexOutOfBoundsException.
I've spent several weeks trying to troubleshoot this problem without posting a question, and I believe I've tried enough potential solutions to justify asking Stack Overflow for guidance. There are countless questions on here with the same subject, but unfortunately none of them have solved my problem. Thank you in advance for your consideration.
Here are some solutions other people have suggested:
To use adapter.notifyDataSetChanged(), but I understand this to
be considered a 'last resort' in the Android documentation
To call list.clear before adapter.notifyDataSetChanged()
To get the position of all current items in the data set to an Integer called 'position' with adapter.getItemCount(), and then pass that to adapter.notifyItemRangeChanged(position)
To set adapter.setHasStableIds(true)
To call mRecyclerView.getRecycledViewPool().clear() and mAdapter.notifyDataSetChanged();
Apparently if the RecyclerView is inside a LinearLayout, 'notify' methods don't work (this might pertain to an old bug in Android which may be fixed now, but I'm not sure.)
All of these suggestions result in a 'Fatal Exception'.
My app uses five files:
JobsAdapter (Adapter)
JobsListItem (Getters and Setters)
JobsOut (Fragment)
jobs_recyclerview
jobs_listitem
I've only included code for the Adapter and Fragment, because I'm confident that the layout files and Getters and Setters are well formed.
Fragment:
public class JobsOut extends Fragment {
String jobId;
String jobTitle;
String jobNumber;
String jobStartTime;
String dispatchType;
#BindView(R.id.jobsOutRecyclerView) RecyclerView jobsOutRecyclerView;
#BindView(R.id.fab) FloatingActionButton refreshFab;
private List<JobsListItem> dispatch;
private RecyclerView.Adapter mJobsOutAdapter;
public RecyclerView.LayoutManager dispatchLayoutManager;
OkHttpClient client = new OkHttpClient();
Handler handler = new Handler();
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.recycler_test, container, false);
ButterKnife.bind(this, rootView);
dispatch = new ArrayList<>();
jobsOutRecyclerView.setHasFixedSize(true);
dispatchLayoutManager = new LinearLayoutManager(getContext());
jobsOutRecyclerView.setLayoutManager(dispatchLayoutManager);
downloadDispatch();
refreshFab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
downloadDispatch();
getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
dispatch.clear();
}
});
}
});
return rootView;
}
#Override
public void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(this);
}
private void downloadDispatch() {
final okhttp3.Request request = new okhttp3.Request.Builder()
.url("url")
.header("X_SUBDOMAIN", "SUBDOMAIN")
.header("X-AUTH-TOKEN", "API_KEY")
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
#Override
public void onFailure(Call call, IOException e) {
}
#Override
public void onResponse(Call call, okhttp3.Response response) throws IOException {
try {
String jsonData = response.body().string();
JSONObject getRootObject = new JSONObject(jsonData);
JSONObject metaObject = getRootObject.getJSONObject("meta");
final String row_count = metaObject.getString("total_row_count");
{
if (row_count.equals("0")) {
// do something for no jobs
} else {
JSONObject getArray = new JSONObject(jsonData);
JSONArray opportunitiesArray = getArray.getJSONArray("opportunities");
for (int i = 0; i < opportunitiesArray.length(); i++) {
JSONObject opportunity = opportunitiesArray.getJSONObject(i);
jobId = opportunity.getString("id");
jobTitle = opportunity.getString("subject");
jobNumber = opportunity.getString("number");
jobStartTime = opportunity.getString("starts_at");
dispatchType = opportunity.getString("customer_collecting");
// Take Strings from response and send them to JobsListItem
final JobsListItem item = new JobsListItem(jobId, jobTitle, jobNumber, jobStartTime, dispatchType);
// If the adapter hasn't been created, do this
if (mJobsOutAdapter == null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
mJobsOutAdapter = new JobsAdapter(dispatch, getContext());
jobsOutRecyclerView.setAdapter(mJobsOutAdapter);
dispatch.add(item);
}
});
}
// If the adapter has been created, just do this
else if (mJobsOutAdapter != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
dispatch.add(item);
mJobsOutAdapter.notifyDataSetChanged();
}
});
}
}
}
}
} catch (IOException e) {
Log.e("TAG", "IO exception caught: ", e);
} catch (JSONException e) {
Log.e("TAG", "TAG exception caught: ", e);
}
}
});
}
Adapter:
public class JobsAdapter extends RecyclerView.Adapter<JobsAdapter.ViewHolder> {
private List<JobsListItem> mJobsListItem;
private Context context;
public JobsAdapter(List<JobsListItem> mJobsListItem, Context context) {
this.mJobsListItem = mJobsListItem;
this.context = context;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.jobs_listitem, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(final ViewHolder holder, int position) {
final JobsListItem mJobsListItemViewHolder = this.mJobsListItem.get(position);
// holders go here and do things with text and what-not
}
#Override
public int getItemCount() {
return mJobsListItem.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
// BindView's with ButterKnife go here and all that jazz
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
Logcat from crash:
26404-26404 E/AndroidRuntime: FATAL EXCEPTION: main
Process: uk.co.plasmacat.techmate, PID: 26404
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 4(offset:4).state:16
at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5504)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5440)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5436)
at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2224)
at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1551)
at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1511)
at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1325)
at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1061)
at android.support.v7.widget.RecyclerView.scrollByInternal(RecyclerView.java:1695)
at android.support.v7.widget.RecyclerView.onTouchEvent(RecyclerView.java:2883)
at android.view.View.dispatchTouchEvent(View.java:10063)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2630)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2307)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2636)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2321)
at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:413)
at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1819)
at android.app.Activity.dispatchTouchEvent(Activity.java:3127)
at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:71)
at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:71)
at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:375)
at android.view.View.dispatchPointerEvent(View.java:10283)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4522)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4353)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3893)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3946)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3912)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4039)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3920)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4096)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3893)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3946)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3912)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3920)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3893)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6341)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6315)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6265)
at
android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6444)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:176)
at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:6415)
at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:6467)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:874)
at android.view.Choreographer.doCallbacks(Choreographer.java:686)
at android.view.Choreographer.doFrame(Choreographer.java:615)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:860)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6290)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
If you have the time, I would thoroughly appreciate your help.
Thank you!
What you’re trying to do is fairly common, your index out of bounds happens when the recycler view needs to ask its adapter for data (because it has scrolled) and the position it needs doesn’t exist in the Adatper. E.g.: the adapter tries to grab item number “N” and the data contains N-1 (or less).
This is most of the times due to a number of factors:
Threading. This should all be handled (for the most part) on the UI Thread (notifications and what not). The network request is obviously happening in a background thread, I think that eventually onResponse is now back on the main thread (otherwise you’d get other exceptions). Double check it my testing Looper.getMainLooper() == Looper.myLooper() (or similar).
You’re doing a lot of (unneeded) work on the Main Thread. You receive the response from the network, and you parse JSON and create objects in the Main Thread… why not offload all the work and once you have a list of items, pass it onto the adapter.
You’re inefficiently calling notifyDataSetChanged() every time (this is bad). Why not use the (included in Android) DiffUtil class to only notify of the changed range? Allow me to point you to a good sample of how it works: https://guides.codepath.com/android/using-the-recyclerview#diffing-larger-changes
It should take you about 30 minutes to implement these changes and it will make your code way more robust.
Bonus points if you use RXJava to make it a stream :-)
note: You should create the adapter once, and then simply call setItems(your_list_of_items) every time you have new data. The DiffUtil and adapter should know how to deal with this. You have a lot of “business logic” there in your activity/fragment/networking code that doesn’t belong there. All your “onResponse” method should do is prepare the data and pass it to the class responsible for managing the data (the adapter). When I see this // If the adapter hasn't been created, do this, I frown. Why is this code creating the adapter here? Who’s gonna test this? What if you change OKHttp with something else? (why not use retrofit and make it even easier?).
I mean, there are multiple things you can do to make your life as a programmer easier, you’re not making use of the solutions available to you.
While there are many good suggestions here. One idea is to just stop the recycler from scrolling when the user interacts with a button that calls additional loading.
recylcerview.stopScroll();
Related
I am calling the filter data function from the fragment search view , it is working fine and the data are getting filtered but the images are getting reloaded.How can this be prevented
public menuadapter(ArrayList<GridItem> mGridDat, Context context, OnItemClickListener listener) {
this.mGridData=new ArrayList<GridItem>();
this.orignallist=new ArrayList<GridItem>();
mGridData.addAll(mGridDat);
orignallist.addAll(mGridDat);
this.context = context;
this.listener = listener;
this.Session=new session(context);
}
public void onBindViewHolder(final MyViewHolder holder, final int position) {
final Activity activity = (Activity)context;
String capital=mGridData.get(position).getTitle().substring(0,1).toUpperCase()+mGridData.get(position).getTitle().substring(1).toLowerCase();
holder.txtview.setText(capital);
Picasso.with(context).load(mGridData.get(position).getImage()).fit().centerCrop().skipMemoryCache().into(holder.imageView);
}
Filter Data function
public void filterData(String query){
query=query.toLowerCase();
//Log.v("check1",String.valueOf(orignallist.size()));
mGridData.clear();
if(query.isEmpty()){
mGridData.addAll(orignallist);
// Log.v("check2",String.valueOf(orignallist.size()));
}
else {
//Log.v("check0",String.valueOf(orignallist.size()));
ArrayList<GridItem> newlist = new ArrayList<>();
for(GridItem gd: orignallist) {
if ((gd.getTitle().toLowerCase().contains(query)) ) {
newlist.add(gd);
}
}
if(newlist.size()> 0){
mGridData.addAll(newlist);
}
}
notifyDataSetChanged();
}
Try this
Picasso.with(context).load(mGridData.get(position).getImage()).fit().centerCrop().networkPolicy(NetworkPolicy.OFFLINE).into(holder.imageView);
You need to use below property and not skipMemoryCache()
OFFLINE
public static final NetworkPolicy OFFLINE
Forces the request through the disk cache only, skipping network.
https://square.github.io/picasso/2.x/picasso/com/squareup/picasso/NetworkPolicy.html
To avoid reloding of images, solutions I think of are
one is to remove the images which are not matching the query and keeping others. Something like -
if(!(gd.getTitle().toLowerCase().contains(query)))
{
// get it from holder.getAdapterPosition();
contentsArrayList.remove(position);
notifyItemRemoved(position);
}
// out of for loop
notifyItemRangeChanged(firstRemovedPostion,contentsArrayList.size());
You can hide the element which is not matching the query and then show if it's matching another
itemView.setVisibility(View.GONE);
I am currently following a tutorial to add real-time location tracking to my application. I have run into some issues since the creator of the tutorial is using firebase-ui-auth:1.2.0 compared to the latest release firebase-ui-auth:4.3.1.
the link to the tutorial is below:
https://www.youtube.com/watch?v=17HqLBkuX-E <-- I am currently stuck at 27:30
I am aware that FirebaseRecyclerAdapter has received some changes, changing populateViewHolder to onBindViewHolder & onCreateViewHolder.
The issue I am having while following this tutorial is, the original code is written as the following:
private void updateList() {
adapter = new FirebaseRecyclerAdapter<UserRace, OnlineListViewHolder>(
UserRace.class,
R.layout.user_layout,
OnlineListViewHolder.class,
counterRef
) {
#Override
protected void populateViewHolder(OnlineListViewHolder viewHolder, UserRace model, int position) {
viewHolder.txtEmail.setText(model.getEmail());
}
};
adapter.notifyDataSetChanged();
onlineList.setAdapter(adapter);
}
while version 4.3.1 is written as:
private void updateList() {
adapter = new FirebaseRecyclerAdapter<UserRace, OnlineListViewHolder>(
UserRace.class,
R.layout.user_layout,
OnlineListViewHolder.class,
counterRef
) {
#Override
protected void onBindViewHolder(#NonNull OnlineListViewHolder holder, int position, #NonNull UserRace model) {
holder.txtEmail.setText(model.getEmail());
}
#NonNull
#Override
public OnlineListViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
return null;
}
};
adapter.notifyDataSetChanged();
onlineList.setAdapter(adapter);
}
I receive an error in this version of the code on the following lines:
UserRace.class,
R.layout.user_layout,
OnlineListViewHolder.class,
counterRef
) {
Red lines will appear under the values within the brackets with it being stated that: FirebaseRecyclerAdapter() in FirebaseRecyclerAdapter cannot be applied to:
Expected Parameters:
Actual Arguments:
Using version 1.2.0 to avoid this error allows this area of the code to function but messes with other areas of my application.
Is there any way for me to contain the required data within the brackets without receiving an error? I have tried to research this issue but I am not proficient in java so I have not been able to find a solution to this.
If anything is unclear with what I have stated, please let me know.
All help will be greatly appreciated.
That code was deprecated and now you need to use FirebaseRecyclerOptions to setup your RecyclerView Adapter, please read the documentation here
https://github.com/firebase/FirebaseUI-Android/blob/master/database/README.md
Now, instead of doing this
adapter = new FirebaseRecyclerAdapter<UserRace, OnlineListViewHolder>(
UserRace.class,
R.layout.user_layout,
OnlineListViewHolder.class,
counterRef
)
you will need to it like this
First, configure the adapter by building FirebaseRecyclerOptions
FirebaseRecyclerOptions<UserRace> options =
new FirebaseRecyclerOptions.Builder<UserRace>()
.setQuery(query, UserRace.class)
.build();
Next create the FirebaseRecyclerAdapter object. You should already have a ViewHolder subclass for displaying each item.
adapter = new FirebaseRecyclerAdapter<UserRace, OnlineListViewHolder>(options) {
#Override
public OnlineListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Create a new instance of the ViewHolder, in this case we are using a custom
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.yourCustomLayout, parent, false);
return new OnlineListViewHolder(view);
}
#Override
protected void onBindViewHolder(OnlineListViewHolder holder, int position, UserRace model) {
// Bind the views
// ...
}
};
And last but not least, this is a very important part, do this at your onStart() and onStop() methods
#Override
protected void onStart() {
super.onStart();
adapter.startListening();
}
#Override
protected void onStop() {
super.onStop();
adapter.stopListening();
}
This is an example, you must adapt it for your use case
I am facing a Firebase RecyclerView problem where I cannot remove unwanted CardViews from my RecyclerViews. In my code I check the city's name and the guide's chosen city to match them. It populates guide's details only if the guide's city matches the picked city, but it also shows empty cardview with default layout.
guideDataRef = FirebaseDatabase.getInstance().getReference().child("Guides");
public void recycler() {
super.onStart();
try {
//Guide RecyclerView
Query guideQuery = guideDataRef.orderByKey();
guideQuery.keepSynced(true);
FirebaseRecyclerOptions guideOptions =
new FirebaseRecyclerOptions.Builder<UserModelClass>().setQuery(guideQuery, UserModelClass.class).build();
guideAdapter = new FirebaseRecyclerAdapter<UserModelClass, guideViewHolder>(guideOptions) {
#Override
protected void onBindViewHolder(#NonNull guideViewHolder holder, final int position, #NonNull final UserModelClass model) {
String pickedcity = model.getPickedCity();
String postname = (String) cityName.getText();
if(pickedcity.equals(postname)) {
final String guide_key= getRef(position).getKey();
holder.setGuideName(model.getName());
holder.setGuideSurname(model.getSurName());
holder.setGuideImage(getApplicationContext(), model.getPhotoURL());
// holder.mView.setVisibility(View.VISIBLE);
//Guide Click listener
holder.mView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent guideHireIntent = new Intent(getApplication(), GuideHireActivity.class);
guideHireIntent.putExtra("guide_id", guide_key);
finish();
startActivity(guideHireIntent);
}
});
}
}
#NonNull
#Override
public guideViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout_guides, parent, false);
return new guideViewHolder(view);
}
#Override
public void onError(DatabaseError e){
Toast.makeText(getApplicationContext(), "Error by stopping ", Toast.LENGTH_SHORT).show();
}
#Override
public int getItemCount() {
return super.getItemCount();
}
#Override
public void onDataChanged() {
super.onDataChanged();
notifyDataSetChanged();
}
};
guideAdapter.notifyDataSetChanged();
guideRecyclerView.setAdapter(guideAdapter);
guideAdapter.startListening();
} catch (DatabaseException e) {
Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
}
}
enter image description here
enter image description here
I can change the adapter visibility to gone if it does not match with the requirements but the problem is that after making it's visibility gone it is still there holding the place (but invisible - there's still an empty space). How can I avoid populating an item from the recycler view completely, instead of making it invisible if the requirements do not match?
You're not showing what guideDataRef is in your code, so I'm assuming that it's just aDatabaseReference object for everything beneath a \Guides node.
If you're doing that, you're going to get a call for onBindViewHolder for every child at that particular location. This means that you're going to be asked to make a view for every child. You cannot choose whether or not a view will appear for that item.
It looks like you're assuming that your if statement in onBindViewHolder method will skip over those items. But what's actually happening is that you're simply allowing an empty view to occupy that spot in the list.
Instead, you should come up with a query that generates only the items of interest to your list. This means you'll have to tell Firebase to filter for children that meet your criteria.
You can also read the entire contents of the location, manually filter out the items you don't want, and build a list of items you do want. You can then build an custom adapter with that list, and it can then become the input to a ListView or even better to a RecyclerView.
Good day all,
I have an issue where my activity is making a network call and when the network call is completed, it makes some changes in the activity using the data from the JSON object received from the call, it then passes the object down to the fragments in the same activity. These fragments are in a TabLayout.
I had this same issue which I asked here at this SO Question That sorted it out but I seem to be having the same issue, even after it worked for a little bit after not changing anything significant. I was just adding more fields I wanted to change?
The issue I have is that if I put a System.out.println() it prints out the correct data. The minute I want to set say a TextView with the data I receive in the Fragment the app Crashes with Nullpointer. When I debug it with the Debug in Android studio, the TextView I'm setting is always null for some reason.
Activity Code that does the initial Network call:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listings);
ButterKnife.bind(this);
setSupportActionBar(toolbar);
handleIntent(getIntent());
}
private void handleIntent(Intent aIntent) {
if (aIntent != null) {
String tradeType = aIntent.getStringExtra("itemType");
String tradeId = aIntent.getStringExtra("itemId");
presenter = new ItemPresenterImpl(this, ItemBuyNowActivity.this);
presenter.doListingServiceCall(tradeId); // <------- This is the where I send the Trade Id so I can do the network call.
} else {
System.out.println("Intent is null in " + ItemBuyNowActivity.class.getSimpleName());
}
}
Interface between Activity and Presenter:
public interface ItemPresenter {
void doListingServiceCall(String itemId); //<------- Comes to this Interface
void doToolbarBackgroundImageCall(TradeItem aTradeItem);
}
Class the implements the Presenter:
#Override
public void doListingServiceCall(String aItemId) { // <------- This is where the network call starts
String homeURL = BobeApplication.getInstance().getWsURL() + mContext.getString(R.string.ws_url_item) + aItemId;
BobeJSONRequest jsObjRequest = new BobeJSONRequest(Request.Method.GET, homeURL, null, this, this);
VolleySingleton.getInstance().addToRequestQueue(jsObjRequest, "ListingRequest");
}
#Override
public void doToolbarBackgroundImageCall(TradeItem aTradeItem) {
ImageRequest request = new ImageRequest(aTradeItem.getItem().getImageUrl(),
new Response.Listener<Bitmap>() {
#Override
public void onResponse(Bitmap bitmap) {
Drawable drawable = new BitmapDrawable(mContext.getResources(), bitmap);
mItemView.loadBackgroundImage(drawable);
}
}, 0, 0, null,
new Response.ErrorListener() {
public void onErrorResponse(VolleyError error) {
mItemView.displayErrorMessage(VolleyErrorHelper.getErrorType(error, mContext) + " occurred downloading background image");
}
});
VolleySingleton.getInstance().addToRequestQueue(request, "ListItemToolbarBackgroundImageRequest");
}
#Override
public void onResponse(Object response) {
Gson gson = new Gson();
TradeItem tradeItem = gson.fromJson(response.toString(), TradeItem.class);
mItemView.populateListViews(tradeItem); // <------- This is the where I send the Object so the views in the activity can be manipulated
doToolbarBackgroundImageCall(tradeItem);
}
Method in the Activity that handles
#Override
public void populateListViews(TradeItem aTradeItem) {
mOverviewPresenter = new OverviewPresenterImpl(new OverviewListItemFragment(), aTradeItem);
OverviewListItemFragment.setData(aTradeItem); //<------- This is the where I send the Object to the fragment so i can manipulate the views in the fragment
}
class TabAdapter extends FragmentPagerAdapter {
public TabAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
Fragment fragment = null;
if (position == 0) {
fragment = new OverviewListItemFragment();
}
if (position == 1) {
fragment = new DescriptionListItemFragment();
}
if (position == 2) {
fragment = new ShippingListItemFragment();
}
if (position == 3) {
fragment = new PaymentListItemFragment();
}
return fragment;
}
#Override
public int getCount() {
return 4;
}
#Override
public CharSequence getPageTitle(int position) {
if (position == 0) {
return "Overview";
}
if (position == 1) {
return "Description";
}
if (position == 2) {
return "Shipping";
}
if (position == 3) {
return "Payment";
}
return null;
}
}
The Fragment that receives the data:
public class OverviewListItemFragment extends Fragment implements OverviewView {
private static TextView mOverViewHeading;
public OverviewListItemFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.overview_list_item_fragment, container, false);
mOverViewHeading = (TextView) view.findViewById(R.id.frag_overview_heading_textview);
return view;
}
#Override
public void populateOverviewViews(final TradeItem aTradeItem) {
System.out.println("Overview Trade Object title is:" + aTradeItem.getItem().getTradeTitle()); // <------- This is print statement works 100% but when I try setting mOverViewHeading to the text in aTradeItem.getItem().getTradeTitle() I get a Null pointer Exception.
}
public static void setData(TradeItem aTradeItem) {
System.out.println("Overview Trade Object title is:" + aTradeItem.getItem().getTradeTitle()); // <------- This is print statement works 100% but when I try setting mOverViewHeading to the text in aTradeItem.getItem().getTradeTitle() I get a Null pointer Exception.
mOverViewHeading.setText(aTradeItem.getItem().getTradeTitle());// <------- This is where it crashes and mOverViewHeading is still null at this point.
}
}
EDIT: Sorry I forgot the LogCat:
02-05 17:08:21.554 30512-30512/com.example.app E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.NullPointerException
at com.example.app.ui.fragments.OverviewListItemFragment.setData(OverviewListItemFragment.java:46)
at com.example.app.ui.activities.ItemBuyNowActivity.populateListViews(ItemBuyNowActivity.java:95)
at com.example.app.listing.ItemPresenterImpl.onResponse(ItemPresenterImpl.java:62)
at com.android.volley.toolbox.JsonRequest.deliverResponse(JsonRequest.java:65)
at com.android.volley.ExecutorDelivery$ResponseDeliveryRunnable.run(ExecutorDelivery.java:99)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5041)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
at dalvik.system.NativeStart.main(Native Method)
My thinking is that the view I'm trying to set isn't "Active" (if thats the right word) at the time it receives the data, because when I run the debugger with a break point at the method that receives the data in the Fragment, the mOverViewHeading TextView id is null, even though I have the findViewById in the onCreate, also tried placing it in the onCreateView() but both times failed. I also tried placing the findViewById in the same method that gets called when the response is successful but before I try setting the setText() on the TextView.
Thank you
OverviewListItemFragment I assume this is not your added fragment instance, but the class.
I suggest the following changes: remove static from setData and your TextView, leave it, if you really know how it works. I don't think it is necessary or recommendable.
private OverviewListItemFragment mFrag; //declare globally
mFrag = new OverviewListItemFragment();
//if you do not want to add it now, ignore the following line
getSupportFragmentManager().beginTransaction().add(R.id.yourContainer, mFrag, "mFrag").commit();
now call mFrag.setData everytime you want to set your data. Check if your mFrag is null, then reinitialize, and maybe re-add, or whatever you want to do.
Edit: Now that I know that you use a ViewPager, I suggest the following:
Do the above. I don't think it is recommendable to have static methods in this Context. You get an error because you are trying to reach a TextView in your Fragment. This was initialized in a ViewPager/PagerAdapter, and the PagerAdapter holds the reference to the used instance of your fragment.
You can access your used fragment through
Fragment mFragment = pagerAdapter.getFragment(0); //frag at position 0
with some casting, you will be able to find your (now NOT static) method:
((OverviewListItemFragment)pagerAdapter.getFragment(0)).setData(YOUR_DATA);
Please add some try/catch. check if your fragment is null, because it is possible that your fragment is recycled in the FragmentPagerAdapter, because it reached the offset. Another way to achieve this, would be to store your required data, and update it everytime your fragment gets visible as described here.
Edit 2: Obviously, You'll need some changed in your Adapter:
I would recommend creating an array containing your fragment in the constructor:
//global in your adapter:
private Fragment[] fragments;
public CustomPagerAdapter(FragmentManager fm) {
super(fm);
fragments = new GameFragment[4];
fragments[0] = new MyFragment();
fragments[1] = new SecondFragment();
....
}
public Fragment getItem(int position) {
return fragments[position];
}
public Fragment getFragment(int position) {
return fragments[position];
}
I have been searching for an answer to my problem, but I seem to get none, despite of how many tutorials I followed, how many questions I've gone through and how many things I've tried to do what I want. Basically, I stumbled upon some good tips, and still couldn't manage to do what wanted.
THE PROBLEM
I am creating an Android Application that will use Fragments (alongside with tabs). In these fragments, I have crucial information relating the application, such as text boxes, and buttons. However, I want to do something really simple, which is updating one of my fragments as I come back to it (imagine I swipe back to a fragment, and I update it with the relevant information). Where is the information stored? On a node.js server, to which I call every time I want information. So for that, I created the following structure.
THE STRUCTURE
First of all, I started off creating my Activity.
public class CentralActivity extends FragmentActivity {
CentralPagerAdapter mCentralActivity;
ViewPager mViewPager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_central);
tabHandler();
}
public void tabHandler() {
mCentralActivity = new CentralPagerAdapter(getSupportFragmentManager());
mViewPager = (ViewPager) findViewById(R.id.CentralPager);
mViewPager.setAdapter(mCentralActivity);
mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
getActionBar().setSelectedNavigationItem(position);
}
});
//Action Bar Stuff
}
}
With this said, I need my CentralPagerAdapter, which I created as follows.
public class CentralPagerAdapter extends FragmentStatePagerAdapter {
private int nSwipes = 3;
public CentralPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int i) {
Fragment fragment = new CentralFragment();
Bundle args = new Bundle();
args.putInt(CentralFragment.ARG_OBJECT, i + 1);
fragment.setArguments(args);
return fragment;
}
#Override
public int getCount() {
return nSwipes;
}
}
And now, my fragment, which is only a class that contains all of my views, and options and so on.
public class CentralFragment extends Fragment {
public static final String ARG_OBJECT = "object";
private View rootView;
private RESTFunction currentFunction;
//Has the info I want
private ArrayList<Integer> tickets = new ArrayList<Integer>();
#SuppressLint("HandlerLeak")
private Handler threadConnectionHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
switch (currentFunction) {
case GET_CLIENT_TICKETS:
handleGetTickets(msg);
break;
case BUY_CLIENT_TICKETS:
break;
default:
break;
}
}
};
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
final Bundle args = getArguments();
handleFragments(inflater, container);
getTicketInfo(null);
return rootView;
}
private void handleFragments(LayoutInflater inflater, ViewGroup container) {
Bundle args = getArguments();
if (args.getInt(ARG_OBJECT) == 1) {
rootView = inflater.inflate(R.layout.fragment_show_tickets,
container, false);
showTicketsHandler();
} else if (args.getInt(ARG_OBJECT) == 2) {
rootView = inflater.inflate(R.layout.fragment_buy_tickets,
container, false);
buyTicketsHandler();
} else {
rootView = inflater.inflate(R.layout.fragment_history_tickets,
container, false);
}
}
public void showTicketsHandler() {
//Get stuff from the tickets array that the REST call will handle
//And set them to boxes or radio buttons
}
public void buyTicketsHandler() {
//Get stuff from the tickets array that the REST call will handle
//And set them to boxes or radio buttons
//As well as button click listeners
}
public void getTicketInfo(ProgressDialog progDialog) {
//Connect to the thread to get the information
//In this case, I have no parameters
ConnectionThread dataThread = new ConnectionThread("myLink", Method.GET, null, threadConnectionHandler, progDialog);
dataThread.start();
}
//Get stuff from the resulting JSON and store it in the tickets ArrayList
private void handleGetTickets(Message msg) {
JSONObject ticketListing = (JSONObject) msg.obj;
try {
tickets.add(ticketListing.getInt("t1"));
tickets.add(ticketListing.getInt("t2"));
tickets.add(ticketListing.getInt("t3"));
} catch (JSONException e) {
e.printStackTrace();
}
}
}
And then, I have my thread..
public class ConnectionThread extends Thread {
private ConnectionRunnable runConnection;
private Handler mHandler;
private ProgressDialog progDialog;
public ConnectionThread(String link, Method method, ArrayList<NameValuePair> payload, Handler handler, ProgressDialog progDialog) {
runConnection = new ConnectionRunnable(link, method.toString(), payload);
mHandler = handler;
this.progDialog = progDialog;
}
#Override
public void run() {
runConnection.run();
threadMsg();
if(progDialog != null)
progDialog.dismiss();
}
public JSONObject getJSON() {
return runConnection.getResultObject();
}
private void threadMsg() {
Message msgObj = mHandler.obtainMessage();
msgObj.obj = getJSON();
mHandler.sendMessage(msgObj);
}
}
And ConnectionRunnable is where I run my HttpURLConnection.
SO WHAT DO I NEED?
Basically, what I'm trying to do, is to get the ticket information from the ConnectionThread BEFORE I load all my view and update them. Plus, I want to be able to swipe back and forth, and update my information on the array as I swipe through the screens (if I go to the second screen, the tickets will update, and if I come back to the first, they will re-update). So basically, call the ConnectionThread everytime I swipe around. If that is possible that, is.
WHAT HAVE I TRIED?
I've tried several things already, and all of them didn't actually help..
The usage of ProgressDialogs to stop the UI Thread on the onCreateView method of the fragment (no use, because it returns the rootView before it handles everything);
Making the UI Thread sleep for 1 second (I don't know why, it blocks all of them);
Overriding the instantiateMethod() of the Adapter, although I think I didn't do it correctly;
Overriding the saveState() of the Adapter, in order to prevent its saved states, and to then get new ticket information;
Giving the fragments tags to update their rootViews on the Adapter, but to no avail;
Getting the information in the activity, and everytime I make a purchase (second fragment), restart the whole activity to get the tickets, which I believe is a really, really bad solution.
I've read several articles, and I still couldn't find my answers.. It's really frustrating. Because it's something so simple, however, the fact that I have to run the HTTP calls on a different thread delays the whole UI updating process.
I've also read the AsyncTask's method. However, I feel like both Threads and AsyncTasks end up in the same.
WHAT TO DO NOW?
Well, that's what I was hoping to find. Because it ends up being annoying as it is.
POSSIBLE REASONS
Is it because I'm separating all classes into spread files, therefore making my work difficult?
Thank you for your time, guys, hope we can find a solution or something.
THE EDIT
So basically, after 4 hours of reading documents and tutorials, I figured that what I needed was setOffscreenPageLimit(int). However, it can't be set to 0, so I will have to do with a setOnPageChangeListener. Now, to figure how to refresh the fragment, and I'll be as good as new.
Alright, it works perfectly! Basically, I did this:
mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
((CentralFragment)((CentralPagerAdapter) mViewPager.getAdapter()).instantiateItem(mViewPager, position)).refresh();
getActionBar().setSelectedNavigationItem(position);
}
});
Where my .refresh is:
public void refresh() {
Bundle args = getArguments();
if (args.getInt(ARG_OBJECT) == 0) {
getTicketInfo(0);
} else if (args.getInt(ARG_OBJECT) == 1) {
getTicketInfo(1);
buyTicketsHandler();
} else {
//To Handle Later
}
}
It's as simple as refreshing the page before you go to it. Why didn't I remember this before..? So, here's the reference for those who ever need this!