0

I am having an issue with a NPE on a clickListener on a RecyclerView which is in a fragment. Initially it works fine with it displaying a toast message, but when I rotate the device and touch any icon I get:

java.lang.NullPointerException: Attempt to invoke interface method 'void com.example.android.newswiz.OnItemClickListener.onItemClick(java.lang.String)' on a null object reference
    at com.example.android.newswiz.Fragments.PublishersSlidePageFragment$PublishersAdapter$PublishersAdapterViewHolder$1.onClick(PublishersSlidePageFragment.java:86)

Here is my Fragment class. I have commented the part where there is the NPE in the logs:

package com.example.android.newswiz.Fragments;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.android.newswiz.OnItemClickListener;
import com.example.android.newswiz.R;


public class PublishersSlidePageFragment extends Fragment {

public OnItemClickListener listener;

public PublishersSlidePageFragment(){}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
                         @Nullable ViewGroup container, @Nullable Bundle 
savedInstanceState) {

    ViewGroup rootView = (ViewGroup) 
inflater.inflate(R.layout.fragment_publishers_slide_page,
            container, false);

    RecyclerView mRecyclerView = 
rootView.findViewById(R.id.recyclerView_publishers);
    int numberOfColumns = 
Integer.parseInt(getContext().getResources().getString(R.string.no_of_cols));
    mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 
numberOfColumns));
    mRecyclerView.setHasFixedSize(true);
    PublishersAdapter mAdapter = new PublishersAdapter();
    mRecyclerView.setAdapter(mAdapter);
    return rootView;
}

public void setListener(OnItemClickListener onItemClickListener) {
        this.listener = onItemClickListener;
}

public  class PublishersAdapter extends RecyclerView.Adapter<PublishersAdapter.PublishersAdapterViewHolder>{


    @NonNull
    @Override
    public PublishersAdapter.PublishersAdapterViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        Context context = parent.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);
        View view = inflater.inflate(R.layout.publisher_item, parent, false);
        return new PublishersAdapterViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull PublishersAdapter.PublishersAdapterViewHolder holder, int position) {
            holder.mPublisherImageView.setImageResource(mThumbIds[position]);
            holder.mPublisherTextView.setText(mPublishers[position]);
            holder.bind(mPublishers[position], listener);
    }

    @Override
    public int getItemCount() {
        return mThumbIds.length;
    }

    public class PublishersAdapterViewHolder extends RecyclerView.ViewHolder {

        private final ImageView mPublisherImageView;
        private final TextView mPublisherTextView;

        public PublishersAdapterViewHolder(View itemView) {
            super(itemView);
            mPublisherImageView = itemView.findViewById(R.id.publisher_icon);
            mPublisherTextView = itemView.findViewById(R.id.publisher_desc);
        }

        public void bind(final String mPublisher, final OnItemClickListener clickListener) {
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    clickListener.onItemClick(mPublisher); //NPE on this line
                }
            });
        }
    }
}

    // references to the images
    private Integer[] mThumbIds = {
            R.drawable.abc_news, R.drawable.abc_news_au,
            R.drawable.aftenposten, R.drawable.aljazeera_english,
            R.drawable.bbc, R.drawable.cbs_news,
            R.drawable.cnn_news, R.drawable.entertainment_weekly,
            R.drawable.espn, R.drawable.financial_post,
            R.drawable.financial_times, R.drawable.fox_news,
            R.drawable.fox_sports, R.drawable.ign,
            R.drawable.independent, R.drawable.lequipe,
            R.drawable.metro, R.drawable.msnbc,
            R.drawable.mtvnews, R.drawable.nat_geo,
            R.drawable.nbc_news, R.drawable.new_scientist,
            R.drawable.new_york_magazine, R.drawable.talk_sport,
            R.drawable.techradar, R.drawable.the_guardian,
            R.drawable.the_nyt, R.drawable.wsj
    };


    private String[] mPublishers = {"ABC News", "ABC News (AU)", "Aftenposten", "AlJazeera (ENG)"
            , "BBC", "CBS News", "CNN News", "Entertainment Weekly", "ESPN", "Financial Post",
            "Financial Times", "Fox News", "Fox Sports", "IGN", "Independent", "L'Equipe",
            "Metro", "MSNBC", "MTV News", "Nat. Geo.", "NBC News", "New Scientist",
            "NY Magazine", "Talk Sport", "TechRadar", "The Guardian", "NYT",
            "Wall Street Journal"};
}

The Fragment class and listener are initialized in the Main Activity:

public class MainActivity extends AppCompatActivity {


private ViewPager mPager;

private SectionsPageAdapter mSectionsPageAdapter;

private Context context;

private TabLayout tabLayout;

private OnItemClickListener publisherClickListener;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    context = getApplicationContext();

   // Login authentication code ....

    //Set up the ViewPager and PagerAdapter

        mSectionsPageAdapter = new 
SectionsPageAdapter(getSupportFragmentManager());
        mPager = findViewById(R.id.sources_container);
        setupViewPager(mPager);

        tabLayout = findViewById(R.id.tabs);
        tabLayout.setupWithViewPager(mPager);


}


//FIXME NPE on listener object if you rotate screen and touch icon

private void setupViewPager(ViewPager mPager) {

    SectionsPageAdapter adapter = new SectionsPageAdapter(getSupportFragmentManager());
    PublishersSlidePageFragment publishersSlidePageFragment = new PublishersSlidePageFragment();

    publisherClickListener = new OnItemClickListener() {
        @Override
        public void onItemClick(String item) {
            Toast.makeText(context, "You have selected " + item, Toast.LENGTH_SHORT).show();
        }
    };

    publishersSlidePageFragment.setListener(publisherClickListener);

    adapter.addFragment(publishersSlidePageFragment, "Publishers");
    adapter.addFragment(new CategoriesSlidePageFragment(), "Categories");
    adapter.addFragment(new CountriesSlidePageFragment(), "Countries");
    mPager.setAdapter(adapter);
}

I find it very weird that it is fine to recreate the fragments when the screen is rotated but is unable to make a new clickListener to the fragment. I have searched StackOverflow and found Save interface (Listener) in onSaveInstanceState to be of use but I don't think in my situation it is good practice to save the listener. I want to make a new listener and just set it on the Recycler View within the fragment. I might be missing something trivial but can anyone advise?

sn87_uk
  • 1
  • 1

2 Answers2

0

I don't have enough rep to comment, so I'll provide what I believe could be the answer...

First, I noticed that there is no reference to the View "itemView" passed into the method "bind(final String mPublisher, final OnItemClickListener clickListener)" and I don't see it globally available. Not sure why it would run once and NPE after screen rotation though.

So, this points me to not saving the fragments instance. Something is being destroyed when you rotate your device, and if the log is correct, it's saying that is the variable mPublisher is being passed in as null.

mPublishers exists the first time the fragment is created, but looks like it's being destroyed / not recreated on an orientation change.

First thing I would try is setRetainInstance(true) to save the fragments instance across config changes. I have been setting it in onCreate(), but I see no reason you can't set it in onCreateView().

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    if (DEBUG) Log.i(TAG, "onCreate(Bundle)");
    super.onCreate(savedInstanceState);

    // retain this fragment across configuration changes
    setRetainInstance(true);
}

If that doesn't do it automatically for you, save the publisher list when the fragment is being recreated. This is done in onSaveInstanceState() of your fragment.

@Override
public void onSaveInstanceState(@NonNull Bundle bundle) {
    if (DEBUG) Log.i(TAG, "onSaveInstanceState(Bundle)");
    // make sure to call the super method so that the states of our views are saved
    super.onSaveInstanceState(bundle);

    // save our own state variables
    bundle.putStringArray("publishers", mPublishers);
}

Then you can restore them in your onCreateView() method.

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

    ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_publishers_slide_page, container, false);

    RecyclerView mRecyclerView = rootView.findViewById(R.id.recyclerView_publishers);
    int numberOfColumns = Integer.parseInt(getContext().getResources().getString(R.string.no_of_cols));
    mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), numberOfColumns));
    mRecyclerView.setHasFixedSize(true);
    PublishersAdapter mAdapter = new PublishersAdapter();
    mRecyclerView.setAdapter(mAdapter);

    // if we have a savedInstanceState, restore the Fragment
    if (savedInstanceState != null) {
        mPublishers = savedInstanceState.getStringArray("publishers");
    }

    return rootView;
}

You may have to do this for other variables as well, such as your mThumbIds array.

Stubbs
  • 11
  • 4
0

The solution was actually a simple fix - I actually needed to define an OnItemClickListener interface in the Fragment class. Then override the onAttach method to set the listener to the context. Previously I just made a separate interface.

So in the Fragment class:

public class PublishersSlidePageFragment extends Fragment {

private OnItemClickListener listener;

public PublishersSlidePageFragment(){}

public interface OnItemClickListener{
    void onItemClick(String item);
}

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    listener = (OnItemClickListener) context;
}
// ... rest of code

Then in the MainActivity, declare that you will implement the PublishersSlidePageFragment.OnItemClickListener:

public class MainActivity extends AppCompatActivity implements PublishersSlidePageFragment.OnItemClickListener {

Then that class needs to implement the listener method:

@Override
public void onItemClick(String item) {
    Toast.makeText(context, "You have clicked " + item, Toast.LENGTH_SHORT).show();
}
sn87_uk
  • 1
  • 1