1

I'm trying to implement PreferenceFragmentCompat and SharedPreferences.OnSharedPreferenceChangeListener.

My app consists of main activity and fragments. The home fragment has a list of URLs with a title, and I would like to add a setting to add a URL to this list. This is what I've tried so far:

Here's the SettingsFragment.java:

@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    Preference preference = findPreference(key);
    if (preference instanceof EditTextPreference) {
        EditTextPreference editTextPreference = (EditTextPreference) preference;
        String value = editTextPreference.getText();
        new HomeFragment().addLink(value);
    } else {
        assert preference != null;
        preference.setSummary(sharedPreferences.getString(key, ""));

    }
}

And the HomeFragment.java:

private ArrayList<LinkItem> urls = new ArrayList<>(Arrays.asList(
        new LinkItem("LifeHacker RSS Feed", "https://lifehacker.com/rss"),
        new LinkItem("Google News Feed", "https://news.google.com/news/rss");
private LinkItemAdapter itemAdapter;
private ListView listView;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_home, container, false);
    listView = view.findViewById(R.id.postListView);
    itemAdapter = new LinkItemAdapter(getActivity(), R.layout.link_item, urls);
    listView.setAdapter(itemAdapter);
    listView.setOnItemClickListener(onItemClickListener);
    itemAdapter.notifyDataSetChanged();
    return view;
}

void addLink(String title) {
    urls.add(new LinkItem(title, "https://google.com"));
    itemAdapter.notifyDataSetChanged();
}

private AdapterView.OnItemClickListener onItemClickListener = new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        HomeFragmentDirections.ActionHomeFragmentToRssFragment action =
                HomeFragmentDirections.actionHomeFragmentToRssFragment(urls.get(position).Link, urls.get(position).Title);
        NavHostFragment.findNavController(HomeFragment.this).navigate(action);
    }
};

If I try doing it like this, the itemAdapter will be null, crashing the app, so I am unsure of how to implement this. If I try recreating it in addLink like in the onCreate method of HomeFragment, the activity ends up being null. If I try passing the activity or the context from settings fragment, the same result occurs.

LinkItemAdapter adapts the following object:

public class LinkItem {
    public String Title;
    public String Link;
}

My results so far have always been the same: crash as soon as I click "OK" on the edit text field after changing it, due to a null pointer. Could anyone help me out with this, please? I am new to android.

Stack trace:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.myfragmentapp, PID: 5185
    java.lang.NullPointerException: Attempt to invoke virtual method 'void com.example.myfragmentapp.adapters.LinkItemAdapter.notifyDataSetChanged()' on a null object reference
        at com.example.myfragmentapp.screens.HomeFragment.addLink(HomeFragment.java:86)
        at com.example.myfragmentapp.screens.SettingsFragment.onSharedPreferenceChanged(SettingsFragment.java:42)
        at android.app.SharedPreferencesImpl$EditorImpl.notifyListeners(SharedPreferencesImpl.java:560)
        at android.app.SharedPreferencesImpl$EditorImpl.apply(SharedPreferencesImpl.java:443)
        at androidx.preference.Preference.tryCommit(Preference.java:1632)
        at androidx.preference.Preference.persistString(Preference.java:1663)
        at androidx.preference.EditTextPreference.setText(EditTextPreference.java:80)
        at androidx.preference.EditTextPreferenceDialogFragmentCompat.onDialogClosed(EditTextPreferenceDialogFragmentCompat.java:99)
        at androidx.preference.PreferenceDialogFragmentCompat.onDismiss(PreferenceDialogFragmentCompat.java:267)
        at android.app.Dialog$ListenersHandler.handleMessage(Dialog.java:1377)
        at android.os.Handler.dispatchMessage(Handler.java:105)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6709)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769)
Arrrow
  • 542
  • 5
  • 21

2 Answers2

0

You should call addLink() after you've created the adapter:

 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
     View view = inflater.inflate(R.layout.fragment_home, container, false);
     listView = view.findViewById(R.id.postListView);
     itemAdapter = new LinkItemAdapter(getActivity(), R.layout.link_item, urls);
     listView.setAdapter(itemAdapter);
     listView.setOnItemClickListener(onItemClickListener);
     addLnk();
     return view;
 }

If you're trying to set a value from one fragment to another you should either use callbacks or a ViewModel, the simpler of those being a callback:

Define a callback inteface:

interface OnSetPreferenceItem{
   void setPrefItemInList(String item);
}

Inside SettingsFragment, define a variable:

 private OnSetPreferenceItem callback;

In the same fragment, fill in the variable in onAttach:

 public void onAttach(Context context) {
     super.onAttach(context);

     callback = (OnSetPreferenceItem )context;
 }

Now instead of calling new HomeFragment().addLink(value);, call

callback.setPrefItemInList(value);

Let your parent activity implement that interface and implement the method suggested:

public void setPrefItemInList(String item){
     homeFragment.addLink(item);
}

Modify your addLink method to protect it:

void addLink(String title) {
   urls.add(new LinkItem(title, "https://google.com"));
    if(itemAdapter!=null){
       itemAdapter.notifyDataSetChanged();
    }
}
Levi Moreira
  • 11,917
  • 4
  • 32
  • 46
  • I'm calling addlink from within a different fragment (SettingsFragment), so this is not possible. The point is to update it from within the onSharedPreferences method within the SettingsFragment. Sorry if that was unclear. – Arrrow May 17 '19 at 21:40
  • The problem remains the same if I do this, itemAdapter will still be null, results in the same error. – Arrrow May 17 '19 at 21:57
0

I would suggest you using the lifecycle functions of the Fragment correctly. When you are modifying some data (i.e. adding a new URL in the list) from another fragment (i.e. SettingsFragment), you do not have to call the HomeFragment.addLink right away actually. Instead, you might consider having the onResume method implemented in your HomeFragment so that when you go back to your HomeFragment, the onResume function is called automatically and there you should update your list and consider calling notifyDataSetChanged on your adapter.

Hence I am trying to provide some pseudo code here. In your SettingsFragment do something like the following.

@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    Preference preference = findPreference(key);
    if (preference instanceof EditTextPreference) {
        EditTextPreference editTextPreference = (EditTextPreference) preference;
        String value = editTextPreference.getText();

        // new HomeFragment().addLink(value); // You do not call this here
        saveTheNewURLInPrefrence(); // Just save the new value in your preference
    } else {
        assert preference != null;
        preference.setSummary(sharedPreferences.getString(key, ""));

    }
} 

Now in your HomeFragment, implement the onResume function like the following.

@Override
protected void onResume() {
    super.onResume();
    urls = getAllItemsFromPreference();

    if(itemAdapter != null) itemAdapter.notifyDataSetChanged();
    else {

        itemAdapter = new LinkItemAdapter(getActivity(), R.layout.link_item, urls);
        listView.setAdapter(itemAdapter);
        listView.setOnItemClickListener(onItemClickListener);
    }

}

To understand more about fragment lifecycle, please check the documentation here. I hope you get the idea.

Reaz Murshed
  • 23,691
  • 13
  • 78
  • 98
  • How would you suggest implementing `saveTheNewURLInPreference()` and `getAllItemsFromPreference()`? – Arrrow May 17 '19 at 22:04
  • [Here's how](https://stackoverflow.com/questions/7057845/save-arraylist-to-sharedpreferences) you can save a list into `SharedPreference`. Once you can save it, I think you can get it as well. – Reaz Murshed May 17 '19 at 22:05
  • If I do this, won't `getActivity()` be null when I call it in `onResume()`? – Arrrow May 17 '19 at 22:29
  • Nope. The `onResume` function is called after `onCreateView`. So you are safe. Check the documentation that I provided in the answer again. – Reaz Murshed May 18 '19 at 00:38
  • Okay, I've tried this but now the line `SharedPreferences prefs = Objects.requireNonNull(getActivity()).getSharedPreferences(getString(R.string.pref_pref2), Context.MODE_PRIVATE);` in `addItemToPreferences` crashes on a nullpointer, since getActivity is null. – Arrrow May 18 '19 at 09:50