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)
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();
}
}
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.
Related
I have a tablayout with a viewpager in my MainActivity.
My PagerAdapter looks like this:
public class MainActivityPagerAdapter extends PagerAdapter {
public MainActivityPagerAdapter(FragmentManager fm, int numOfTabs) {
super(fm, numOfTabs);
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StoresFragment();
case 1:
return new OrdersFragment();
default:
return null;
}
}
}
I am coming back from another activity like this:
Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish(); //finishAffinity();
But then I get an java.lang.IllegalStateException in one of my Fragments in the viewpager of the MainActivity.
I read many related questions and tried to solve this. It is said, that this happens when one keeps references to Fragments outside of the PagerAdapter. But I am not doing this, as you can see in my code.
Does anyone know what I am doing wrong?
Edit - Stacktrace
FATAL EXCEPTION: main
Process: com.lifo.skipandgo, PID: 23665
java.lang.IllegalStateException: Fragment OrdersFragment{42c2a740} not attached to a context.
at android.support.v4.app.Fragment.requireContext(Fragment.java:614)
at android.support.v4.app.Fragment.getResources(Fragment.java:678)
at com.lifo.skipandgo.activities.fragments.OrdersFragment$1.results(OrdersFragment.java:111)
at com.lifo.skipandgo.connectors.firestore.QueryResult.receivedResult(QueryResult.java:37)
at com.lifo.skipandgo.controllers.UserController$2.onUpdate(UserController.java:88)
at com.lifo.skipandgo.connectors.firestore.QuerySubscription.onEvent(QuerySubscription.java:59)
at com.lifo.skipandgo.connectors.firestore.QuerySubscription.onEvent(QuerySubscription.java:18)
at com.google.firebase.firestore.zzg.onEvent(Unknown Source)
at com.google.firebase.firestore.g.zzh.zza(SourceFile:28)
at com.google.firebase.firestore.g.zzi.run(Unknown Source)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:146)
at android.app.ActivityThread.main(ActivityThread.java:5653)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)
at dalvik.system.NativeStart.main(Native Method)
Edit:
Interesting is, that the view has defenitely loaded when the error occurs. Because the error occurs about 10-15 seconds later after the fragment is shown again. I this in my orderFragment, where the error occurs:
orders = new QueryResult<UserOrder>(UserOrder.class) {
#Override
public void results(List<UserOrder> results) {
orderLoadingMessage.setBackgroundColor(getResources().getColor(R.color.green));
}
}
I do this in onCreateView and this result comes about 10-15 seconds after the view loaded.
The problem seems to be, that your fragment is listening to some events (via UserController and QueryResult) and these are fired before the fragment is attached to context.
Try to unregister the fragment when it becomes detached and to them again after attaching (LiveData can also help with this). Another way could be to receive and store the event while detached and only process it after attaching.
Use this before update your Activity UI :
if(isAdded())// This {#link androidx.fragment.app.Fragment} class method is responsible to check if the your view is attached to the Activity or not
{
// TODO Update your UI here
}
viewPager.offscreenPageLimit = (total number of fragments - 1)
viewPager.adapter = Adapter
Use this if your are using viewpager
And if you are using bottom navigation just simply check if(context != null)
But i suggest to use max 3 fragments in offscreenPageLimit
Some of your callbacks are being fired after your fragment is detached from activity. To resolve this issue you need to check whether your fragment is added before acting upon any callbacks. For example, change your orders object's initialization to this:
orders = new QueryResult<UserOrder>(UserOrder.class) {
#Override
public void results(List<UserOrder> results) {
if(isAdded()) {
orderLoadingMessage.setBackgroundColor(
getResources().getColor(R.color.green));
}
}
}
In my case this exception happened when I showed a DialogFragment and called it's methods. Because the fragment hasn't attached to a FragmentManager (this operation completes asynchronously) before calling methods, an application crashed.
val fragment = YourDialogFragment.newInstance()
fragment.show(fragmentManager, YourDialogFragment.TAG)
// This will throw an exception.
fragment.setCaptions("Yes", "No")
If you add the fragment with FragmentManager, you will get another exception: java.lang.IllegalArgumentException: No view found for id 0x1020002 (android:id/content) for fragment (or similar, if you use another id).
You can call fragment methods via post (or postDelayed), but it is a bad solution:
view?.post {
fragment.setCaptions("Yes", "No")
}
Currently I use childFragmentManager instead of fragmentManager:
val fragment = YourDialogFragment.newInstance()
fragment.show(childFragmentManager, YourDialogFragment.TAG)
fragment.setCaptions("Yes", "No")
I don't remember what I did, but now it works.
I had similar problem. I have solved it by following ferini's recommendation. I was using a live data which was firing before the context was attached.
Here is my full implementation
public class PurchaseOrderFragment extends Fragment {
FragmentPurchaseOrderBinding binding;
CurrentDenominationViewModel currentDenominationViewModel;
#Inject
ViewModelFactory viewModelFactory;
CurrentVoucherChangedObserver observer;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
currentDenominationViewModel = new ViewModelProvider(requireActivity(),viewModelFactory).get(CurrentDenominationViewModel.class);
observer = new CurrentVoucherChangedObserver();
}
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_purchase_order, container, false);
return binding.getRoot();
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
currentDenominationViewModel.getCurrentVoucherStatisticsLiveData().observe(requireActivity(), observer);
}
#Override
public void onAttach(#NonNull Context context) {
AndroidSupportInjection.inject(this);
super.onAttach(context);
}
#Override
public void onDetach() {
super.onDetach();
currentDenominationViewModel.getCurrentVoucherStatisticsLiveData().removeObserver(observer);
}
final class CurrentVoucherChangedObserver implements Observer<VoucherStatistics> {
#Override
public void onChanged(VoucherStatistics x) {
String denomination = x.getDenomination()+"";
binding.tvDenomination.setText(denomination);
String stockAmount = requireContext().getResources().getString(R.string.StockAmount);
String text= "("+String.format(stockAmount,x.getQuantity()+"")+")";
binding.tvInStock.setText(text);
}
}
}
Your Solution
change your getItem() method to
switch (position) {
case 0:
return new StoresFragment();
case 1:
return new OrdersFragment();
default:
return null;
}
I occasionally get NullPointerException when entering fragment. It happens when the app was in the background for a long time and then I open it and swipe to this fragment.
public class SummaryFragment extends Fragment implements FragmentLifecycle {
private static final String TAG = "DTAG";
private DateFormat dateFormatName;
private Preference prefs;
private List<String> monthList;
private TextView totalTimeFullTv;
private TextView totalTimeNetTv;
private TextView averageTimeTv;
private TextView overUnderTv;
private TextView minTimeTv;
private TextView maxTimeTv;
private TextView vacationsTv;
private TextView sickTv;
private TextView headlineTv;
private TextView overUnderTvH;
private OnFragmentInteractionListener mListener;
public SummaryFragment() {
// Required empty public constructor
}
public static SummaryFragment newInstance(String param1, String param2) {
SummaryFragment fragment = new SummaryFragment();
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View RootView = inflater.inflate(R.layout.fragment_summary, container, false);
dateFormatName = new SimpleDateFormat(getResources().getString(R.string.month_text));
monthList = Arrays.asList(new DateFormatSymbols().getMonths());
prefs = new Preference(GeneralAdapter.getContext());
totalTimeFullTv = RootView.findViewById(R.id.textView_sum_ttf);
totalTimeNetTv = RootView.findViewById(R.id.textView_sum_ttn);
averageTimeTv = RootView.findViewById(R.id.textView_sum_av);
overUnderTv = RootView.findViewById(R.id.textView_sum_ou);
overUnderTvH = RootView.findViewById(R.id.textView_sum_ou_h);
minTimeTv = RootView.findViewById(R.id.textView_sum_min);
maxTimeTv = RootView.findViewById(R.id.textView_sum_max);
vacationsTv = RootView.findViewById(R.id.textView_sum_vac);
sickTv = RootView.findViewById(R.id.textView_sum_sick);
headlineTv= RootView.findViewById(R.id.textView_sum_headline);
return RootView;
}
private void refreshData() {
if (prefs == null)
{
prefs = new Preference(GeneralAdapter.getContext());
}
String month = prefs.getString(Preference.CURRENT_MONTH);
MonthData monthData = Calculators.CalculateLocalData(MainActivity.db.getAllDays(month));
totalTimeFullTv.setText(monthData.getTotalTimeFull()); //Crash here
totalTimeNetTv.setText(monthData.getTotalTimeNet());
averageTimeTv.setText(monthData.getAverageTime());
overUnderTv.setText(monthData.getOverUnder());
if (monthData.getOverUnderFloat()<0)
{
overUnderTvH.setText(R.string.sum_over_time_neg);
overUnderTv.setTextColor(ContextCompat.getColor(GeneralAdapter.getContext(),R.color.negative_color));
}
else
{
overUnderTvH.setText(R.string.sum_over_time_pos);
overUnderTv.setTextColor(ContextCompat.getColor(GeneralAdapter.getContext(),R.color.positive_color));
}
minTimeTv.setText(monthData.getMinTime());
maxTimeTv.setText(monthData.getMaxTime());
vacationsTv.setText(""+monthData.getVacations());
sickTv.setText(""+monthData.getSick());
headlineTv.setText(month);
}
public void onButtonPressed(Uri uri) {
if (mListener != null) {
mListener.onFragmentInteraction(uri);
}
}
#Override
public void onAttachFragment(Fragment childFragment) {
super.onAttachFragment(childFragment);
}
#Override
public void onDetach() {
super.onDetach();
mListener = null;
}
#Override
public void onPauseFragment() {
}
#Override
public void onResumeFragment()
{
refreshData();
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
}
}
MainActivity viewPager:
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
int currentPosition = 0;
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
FragmentLifecycle fragmentToHide = (FragmentLifecycle) adapter.getItem(currentPosition);
fragmentToHide.onPauseFragment();
FragmentLifecycle fragmentToShow = (FragmentLifecycle) adapter.getItem(position);
fragmentToShow.onResumeFragment(); //Crash start
currentPosition = position;
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
Log:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: michlind.com.workcalendar, PID: 25038
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
at michlind.com.workcalendar.mainfragments.SummaryFragment.refreshData(SummaryFragment.java:99)
at michlind.com.workcalendar.mainfragments.SummaryFragment.onResumeFragment(SummaryFragment.java:147)
at michlind.com.workcalendar.activities.MainActivity.onPageSelected(MainActivity.java:84)
at android.support.v4.view.ViewPager.dispatchOnPageSelected(ViewPager.java:1941)
at android.support.v4.view.ViewPager.scrollToItem(ViewPager.java:680)
at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:664)
at android.support.v4.view.ViewPager.onTouchEvent(ViewPager.java:2257)
at android.view.View.dispatchTouchEvent(View.java:11776)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2962)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2643)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2657)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2657)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2657)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2657)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2657)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2657)
at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:448)
at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1829)
at android.app.Activity.dispatchTouchEvent(Activity.java:3307)
at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:68)
at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:410)
at android.view.View.dispatchPointerEvent(View.java:12015)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4795)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4609)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4200)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4166)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4293)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4174)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4350)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4200)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4166)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4174)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6661)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6635)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6596)
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6764)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:186)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:325)
at android.os.Looper.loop(Looper.java:142)
at android.app.ActivityThread.main(ActivityThread.java:6494)
UPDATE:
I eventually used:
#Override
public void onPageSelected(int position) {
Fragment fragment = adapter.getFragment(position);
if (fragment != null) {
fragment.onResume();
}
}
At my MainActivity, and used onResume() at each fragment. And this solution for the adapter:
http://thedeveloperworldisyours.com/android/update-fragment-viewpager/
The problem is, that you are trying to access views too early: view hierarchy is not created at that point yet.
If you post an event, that would take place on the next frame, you are guaranteed, that view hierarchy would be already setup:
#Override
public void onResumeFragment() {
new Handler().post(new Runnable() {
#Override
public void run() {
refreshData();
}
});
}
I faced the same problem when I had implemented custom life cycles for ViewPager. I think you are using FragmentStatePagerAdapter to populate fragments with ViewPager. As we know FragmentStatePagerAdapter destroys all the fragments when they lose focus. We need to provide same object for every page using singleton pattern.
In your code, Fragment creation can be like below for singleton pattern.
private SummaryFragment mInstance;
private SummaryFragment() {
// Required empty public constructor
}
public static SummaryFragment newInstance(String param1, String param2) {
if(mInstance == null)
mInstance = new SummaryFragment();
return mInstance;
}
Doing this has solved my problem. If this does not work for you? Can you share your PagerAdapter class.
onResumeFragment() is getting invoked before the creation of all the views of this fragment.
Try recreating newInstance first and then invoke onResumeFragment of FragmentLifeCycle interface in your Activity.
ViewPager keeps several items on either side attached (i.e. fragments resumed), however FragmentPagerAdapter uses Fragment.setUserVisibleHint to indicate, which item is current. Leverage that instead.
Here's what to do to leverage user visible hint:
Remove the OnPageChangeListener.
Ditch the FragmentLifecycle interface.
Set your fragment like so:
(in Kotlin, but you'll get the gist)
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser && isResumed) {
// Do stuff.
}
}
override fun onResume() {
super.onResume()
if (userVisibleHint) {
// Do the same stuff.
}
}
More info
FragmentPagerAdapter.getItem is a factory method. It's always supposed to return you a new instance of a fragment. If you tried to cache them, remove the cache (1) and don't use getItem yourself (2).
Code that sometimes crashes and sometimes doesn't is a b**** to debug. This can be caused by reusing fragments when you're not supposed to.
A new fragment instance is not attached, has no reason to create views and will be garbage collected once you leave onPageSelected.
You are using OnPageChangeListener incorrectly. This is not a safe way to control view lifecycle events. You need to use PagerAdapter in conjunction with ViewPager and override its instantiateItem / destroyItem callbacks.
See this example: http://android-er.blogspot.com/2014/04/example-of-viewpager-with-custom.html
PagerAdapter is to ViewPager what ListAdapter is to ListView, you need both to make your system work correctly.
Use onViewCreated() callback method of fragment to update your data that way you are sure that all your views are laid out perfectly.
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
refreshData();
}
Using Handler can still be risky as you can't be sure the view are inflated or not.
PROBLEM
The lifecycle of a Fragment is independent. You cannot be sure that when an onPageSelected() gets registered, that fragment has already been laid out. It's is an asynchronous event. So you cannot rely on this callback.
But on the other hand, you cannot also rely only on the onResume(), since in a ViewPager, the pages adjacent to the currently visible page are preloaded.
SOLUTION
Principally you will need to refreshData() when the fragment is visible to user and actively running. The definition of onResume() says the same:
Called when the fragment is visible to the user and actively running. (...)
So simply call refreshData() in the onResume() of your fragment, and don't worry if you notice this getting called while the ViewPager wasn't really showing this page.
Like most people said you need to make sure your fragment is active and visible to the user. i had a similar problem. I used onHiddenChanged to decided when to reload data.
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden) {
refreshData();
}
}
You should inflate your layout in onCreateView but shouldn't initialize other views using findViewById in onCreateView.
here is a code from the FragmentManager
// This calls onCreateView()
f.mView = f.performCreateView(f.getLayoutInflater(f.mSavedFragmentState), null, f.mSavedFragmentState);
// Null check avoids possible NPEs in onViewCreated
// It's also safe to call getView() during or after onViewCreated()
if (f.mView != null) {
f.mView.setSaveFromParentEnabled(false);
if (f.mHidden) f.mView.setVisibility(View.GONE);
f.onViewCreated(f.mView, f.mSavedFragmentState);
}
It's better to do any assignment of subviews to fields in onViewCreated. This is because the framework does an automatic null check for you to ensure that your Fragment's view hierarchy has been created and inflated (if using an XML layout file) properly.
once the view is created then initialize your views.
Add this check in refreshData() method:
if (isAdded() && getActivity() != null)
Maybe I am missing something small here, but I cannot get my binding to work. I set it up as follow:
public class Toolbar extends Fragment {
//Interaction handlers
//interface for interaction with Activity
public interface ToolBarInteraction{
public void Search(String screenName);
}
private ToolbarBind modelData;
private ToolBarInteraction mListener;
public static Toolbar newInstance() {
return new Toolbar();
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentToolbarBinding binding = DataBindingUtil.setContentView(getActivity(), R.layout.fragment_toolbar);
modelData = ToolbarBind.newInstance();
modelData.searchedText.set("Hello");
binding.setModelData(modelData);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
checkMListener();
View view = inflater.inflate(R.layout.fragment_toolbar, container, false);
//get button to set onClick event
Button button = (Button)view.findViewById(R.id.btnSearch);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
String hello = modelData.searchedText.get();
}
});
return view;
}
public void OnSearchClicked(View view){
mListener.Search(modelData.searchedText.get());
}
private void checkMListener(){
try{
mListener = (ToolBarInteraction) getActivity();
} catch (ClassCastException ex) {
throw new ClassCastException(getActivity().toString()
+ " must implement the ToolBarInteraction Interface");
}
}
}
Here is the code for ToolbarBind:
public class ToolbarBind extends BaseObservable {
private String _searchText;
public final ObservableField<String> searchedText = new ObservableField<String>();
//factory method
public static ToolbarBind newInstance(){ return new ToolbarBind(); }
}
And in my fragment, I set the binding up as follow, all within the layout tag:
<data>
<variable
name="modelData"
type="Common.CommonObjects.ToolbarBind"/>
</data>
And for binding to property:
<EditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Search"
android:text="#={modelData.searchedText}"/>
As can be seen, in the onCreate I set the text to "Hello", but even when the view displays on the phone, the EditText is not populated with this text. When I change the value, and click my button, the value I get back in the event is "Hello", not my new text entered while the app is running.
What am I missing?
The problem with your code is that you set the Activity's content view to something in onCreate(...) but you inflate and use something different in onCreateView(...) as your fragment's view, which gets the model data (not the other one you created in onCreate(...)). I don't know exactly what you try to achieve, but I'm gonna guess that you don't want to change the Activity's content view to something from the fragment, so I'm just gonna show you a variation of what could you use, however, you should change it to whatever pleases you.
Remove onCreate(...) completely then use only onCreateView(...) to inflate the fragment_toolbar layout using data binding:
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
checkMListener();
FragmentToolbarBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_toolbar, container, false);
// FIXME you're losing data here; watch out: checking whether savedInstanceState == null is not enough because returning from backstack it will be null
modelData = ToolbarBind.newInstance();
// FIXME modify this so it sets the data from savedInstanceState when configuration changes
modelData.searchedText.set("Hello");
binding.setModelData(modelData);
//get button to set onClick event
binding.btnSearch.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
String hello = modelData.searchedText.get();
}
});
return binding.getRoot();
}
Watch out for the FIXME parts. You could move the modelData init to onCreate(...) which would save it from the backstack-return thing, however, configuration change will still call onCreate(...) unless you call setRetainInstance(true) on the fragment (do not).
I have an Activity A with a fragment frag2. Inside the fragment I have a RecyclerView and Adapter to show a list of custom class objects. Adding objects to the adapter is handled programmatically. I have a button inside TwoFragment that opens a FragmentDialog. I'd like to add an object to my Adapter by confirming this dialog, but it seems that the adapter is null when called from the FragmentDialog.
The same adapter is not null, and works if I call it from the fragment OnClick.
Moreover the adapter is null only after screen rotation, it works fine before rotating.
To communicate between the two Fragments I implement a communicator class in activity A.
Activity A
public void respond(String type) {
frag2.addSupport(type);
}
frag2
public RecyclerView rv;
public ArrayList<support> supports;
public myAdapter adapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
supports = new ArrayList<>();
adapter = new myAdapter(supports);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View layout = inflater.inflate( R.layout.fragment_two, container, false);
layout.setId(R.id.frag2);
if (savedInstanceState!=null)
{
supports = savedInstanceState.getParcelableArrayList("supports");
}
rv = (RecyclerView) layout.findViewById(R.id.rv);
adapter = new myAdapter(supports);
rv.setAdapter(myAdapter);
rv.setLayoutManager(new LinearLayoutManager(getActivity()));
rv.setItemAnimator(new DefaultItemAnimator());
#Override
public void onClick(View v) {
int id = v.getId();
switch (id){
case R.id.button1:
addSupport(type); // THIS WORKS ALWAYS, even after screen rotate
break;
case R.id.button2:
showDialog();
break;
}
}
public void showDialog(){
FragmentManager manager = getFragmentManager();
myDialog dialog = new myDialog();
dialog.show(manager, "dialog");
}
public void addSupport(String type){
adapter.addItem(new support(type)); // this line gives null pointer on adapter, but only if called after screen rotate and only if called from the dialog
}
dialog
communicator comm;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog, null);
comm = (myCommunicator) getActivity();
create = (Button) view.findViewById(R.id.button_ok);
create.setOnClickListener(this);
return view;
}
#Override
public void onClick(View v) {
if(v.getId()==R.id.button_ok)
{
// some controls to set type
comm.respond(type)
dismiss();
}
else {
dismiss();
}
myAdapter
public class myAdapter extends RecyclerView.Adapter<myAdapter.VH> {
private LayoutInflater inflater;
private ArrayList<support> data = new ArrayList<>();
// settings for viewholder
public myAdapter (ArrayList<support> data)
{
this.data=data;
}
public void addItem(support dataObj) {
data.add(dataObj);
notifyItemInserted(data.size());
}
}
logcat
FATAL EXCEPTION: main
java.lang.NullPointerException: Attempt to invoke virtual method 'myAdapter.addItem(myObject)' on a null object reference
I hope there are no mistakes, I shortened the code for better understanding. Keep in mind that everything works if I never rotate the screen.
I'm a beginner with android and I'm stuck with this for several days now. Please, help.
To understand the problem, it's as you say:
.. everything works if I never rotate the screen
So firstly to understand what happens on rotation, this is a quote from the Android Developer website:
Caution: Your activity will be destroyed and recreated each time the user rotates the screen. When the screen changes orientation, the system destroys and recreates the foreground activity because the screen configuration has changed and your activity might need to load alternative resources (such as the layout).
Ok, now to understand the error:
FATAL EXCEPTION: main
java.lang.NullPointerException: Attempt to invoke virtual method 'myAdapter.addItem(myObject)' on a null object reference
Essentially, in your dialog class, you have created a strong dependency by declaring :
comm = (myCommunicator) getActivity();
because comm references objects which would have been destroyed on rotation, hence the NullPointerException.
To further understand runtime changes, such as orientation changes, I'd recommend going through Handling Runtime Changes.
Update
Thank you for your answer, what would you recommend instead of comm = (myCommunicator) getActivity(); ?
The solution comes in 3 parts:
Make sure the onCreate of Activity A has the following:
#Override
public void onCreate(Bundle savedInstanceState) {
......
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
frag2 = (Frag2) fm.findFragmentByTag(“frag2”);
// create frag2 only for the first time
if (frag2 == null) {
// add the fragment
frag2 = new Frag2();
fm.beginTransaction().add(frag2 , “frag2”).commit();
}
......
}
Add setRetainInstance(true) to the onCreate of frag2.
Remove the implicit referencing i.e. comm = (myCommunicator) getActivity();, and implement something more loosely coupled for dialog.
dialog
public interface Communicator {
void respond(String type);
}
Communicator comm;
....
public void addCommunicator(Communicator communicator) {
comm = communicator;
}
public void removeCommunicator() {
comm = null;
}
#Override
public void onClick(View v) {
if((v.getId()==R.id.button_ok) && (comm!=null))
{
// some controls to set type
comm.respond(type);
}
// Regardless of what button is pressed, the dialog will dismiss
dismiss();
}
This allows you do the following in frag2 (or any other class for that matter):
frag2
<pre><code>
public class Frag2 extends Fragment implements dialog.Communicator {
........
public void showDialog() {
FragmentManager manager = getFragmentManager();
myDialog dialog = new myDialog();
dialog.addCommunicator(this);
dialog.show(manager, "dialog");
}
#Override
public void respond(String type){
adapter.addItem(new support(type));
}
}
I am having trouble figuring out how to share data between my two fragments which are hosted on the same activity.
The objective:
I want to transfer string from the the selected position of a spinner and an image url string from a selected list view position from fragment A to fragment B.
The Attempt:
I read the fragments doc on this problem here http://developer.android.com/guide/components/fragments.html#CommunicatingWithActivity
And went ahead an created the following Interface to use betweeen the Fragments and the Host Activity.
public interface OnSelectionListener {
public void OnSelectionListener(String img, String comments );
}
Then I proceeded to implement it in my fragment A's onCreateView method like so:
postList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
ListData link = data.get(position);
String permalink = link.getComments();
String largeImg = link.getImageUrl();
Fragment newFragment = new DetailsView();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
//pass data to host activity
selectionListener.OnSelectionListener(permalink,largeImg);
}
});
And also in the onAttach method
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
selectionListener = (OnSelectionListener)getActivity();
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement onSelectionListener");
}
}
In the Host activity I implemented the interface I wrote and overrided the method like so:
#Override
public void OnSelectionListener(String img, String comments) {
DetailsView detailsView = new DetailsView();
DetailsView dView = (DetailsView)getSupportFragmentManager().findFragmentByTag(detailsView.getCustomTag());
dView.setInformation(img, comments);
}
In Fragment B I set a "tag" the following way
private String tag;
public void setCustomTag(String tag)
{
this.tag = tag;
}
public String getCustomTag()
{
return tag;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setCustomTag("DETAILS_VIEW");
And my thinking is that that the information can be passed to Fragment B by calling this method from the host activity
void setInformation (String info, String img){
RedditDetailsTask detailsTask = new RedditDetailsTask(null,DetailsView.this);
detailsTask.execute(info);
setDrawable(img);
}
What I need:
I want to know how to properly use tags to get this to work, I dont have any fragment id's declared in my xml and rather opted to exchange fragments in a fragment_container.
I also am not sure if this is a good way to pass multiple strings between fragments. I am a newbie programmer so I know my logic probably looks pretty embarrassing but I am trying to do my best learn to do this right. I would appreciate it if you more senior developers can point me in the right direction for doing this.
You don't need to use tags. Take a look at this example. The Activity implements an interface that allows you to talk from Fragment1 back to the Activity, the Activity then relays the information into Fragment2.
I've left out all the android stuff about FragmentManager etc.
interface FragmentListener {
void onTalk(String s1);
}
public class MyActivity extends Activity implements FragmentListener {
Fragment2 fragment2;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
// Find fragment2 and init
}
#Override
public void onTalk(String s1) {
fragment2.onListen(s1);
}
private static class Fragment1 extends Fragment {
private FragmentListener communication;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
communication = (FragmentListener) activity;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_one, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// or in an onClick listener
communication.onTalk("blah blah");
}
}
private static class Fragment2 extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_two, container, false);
}
public void onListen(String s1) {
Log.d("TADA", s1);
}
}
}
My approach would be, when you get the callback in activity through the OnSelectionListener interface, I would create the Fragment B object and set arguments to it as follows:
#Override
public void OnSelectionListener(String img, String comments) {
DetailsView detailsView = new DetailsView();
Bundle args=new Bundle();
args.putString("img",img);
args.putString("comments",comments);
detailsView.setArguments(args);
//code here to replace the fragment A with fragment B
}
Then in Fragment B's onCreate method you can retrieve the values as follows:
#Override
public void onCreate(Bundle savedInstanceState) {
Bundle args=getArguments();
String img=args.getString("img");
String comments=args.getString("comments");
//do whatever you want to do with the varaibles
}
You could try to make two public static String's in your B fragment.
it Would look like something like this
public static String img;
public static String comment;
The you set the variables before making the transaction to fragment B
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
SecondFragment.img = new String("imgString"); //Making a new string so incase you change the string in bfragment, the values wont change in here
SecondFragment.comment = new String("comment");
// Commit the transaction
transaction.commit();
Then in the onStop(), or onDestroy() - depending on when you want the variables to be null, check this - you set the the static variables to null, so they dont take memory space
public void onDestroy(){
super.onDestroy();
img = null;
comment = null;
}