Occasional NPE when accessing fragment's view - java

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)

Related

Android update listview after changing data using sharedpreferences

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.

java.lang.IllegalStateException: Fragment not attached to a context

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;
}

Fragments not visible when switch tabs using ViewPager and TabLayout

I have a HostActivity that uses ViewPager and TabLayout to switch between multiple Fragments. When I switch between the tabs, the Fragments instance does get the updated data. I also see the updated data in onCreateView of the Fragment instance, but the TextView.setText does not get updated. When I check the visibility of Fragment, it always shows Invisible. How do I make the fragment visible when I switch tabs so that the view gets updated with new data? Is there something missing in the Fragment/Activity Lifecycle? I am implementing ViewPager for the first time so it will be helpful to know if I am missing something.
Fragment Class:
public class StepFragment extends Fragment { #Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (getArguments() != null) {
step = getArguments().getParcelable(SELECTED_STEP);
mDescription = step.getDescription();
}
View view = inflater.inflate(R.layout.step_fragment, container, false);
ButterKnife.bind(this,view);
Log.e(TAG, "onCreateView: "+mDescription); **// THIS GETS UPDATED DATA**
tvStepDescription.setText(mDescription);
}
return view;
}
}
Here is my Host Activity:
public class StepActivity extends AppCompatActivity {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_step);
fragmentSelectAdapter = new StepFragmentSelectAdapter(getSupportFragmentManager(),this,steps,recipe);
mViewPager.setAdapter(fragmentSelectAdapter);
mTabLayout.setupWithViewPager(mViewPager);
stepFragment = (StepFragment) getSupportFragmentManager().findFragmentById(R.id.step_container);
if(stepFragment == null) {
stepFragment = StepFragment.newInstance(step, recipe);
stepFragment.setArguments(bundle);
getSupportFragmentManager().beginTransaction()
.add(R.id.step_container, stepFragment)
.commit();
} else {
stepFragment = StepFragment.newInstance(step, recipe);
stepFragment.setArguments(bundle);
getSupportFragmentManager().beginTransaction()
.replace(R.id.step_container, stepFragment)
.commit();
}
}
}
Here is my FragmentPagerAdapter, which seems to be getting the correct data as per the tab position in getItem method:
public class StepFragmentSelectAdapter extends FragmentPagerAdapter {
...
#Override
public Fragment getItem(int position) {
**// THIS GETS UPDATED DATA**
Log.e(TAG, "getItem: \nDecr: "+steps.get(position).getDescription()+"\nVideo: "+steps.get(position).getVideoURL()+"\nImage: "+steps.get(position).getThumbnailURL());
return StepFragment.newInstance(steps.get(position),recipe);
}
#Override
public int getCount() {
if (steps == null){
return 0;
}
return steps.size();
}
...
}
As far as I could understand about the problem that you are having there, I think you should implement an onResume function in your StepFragment which will get the updated data from some source and will display this in the TextView. However, I can think of a potential problem in your StepFragmentSelectAdapter. You are creating a new instance each time you are switching the tabs.
You should have the Fragment instances created before and if you are about to pass the data among fragments, you might consider having a BroadcasReceiver or listener function by implementing an interface.
So the PagerAdapter should look something like this.
public class StepFragmentSelectAdapter extends FragmentPagerAdapter {
public ArrayList<StepFragment> stepFragments;
public StepFragmentSelectAdapter(ArrayList<Step> steps) {
stepFragments = new ArrayList<>();
for(int i = 0; i < steps.size(); i++) {
stepFragments.add(StepFragment.newInstance(steps.get(position),recipe));
}
}
...
#Override
public Fragment getItem(int position) {
**// THIS GETS UPDATED DATA**
Log.e(TAG, "getItem: \nDecr: "+steps.get(position).getDescription()+"\nVideo: "+steps.get(position).getVideoURL()+"\nImage: "+steps.get(position).getThumbnailURL());
return stepFragments.get(position);
}
#Override
public int getCount() {
if (steps == null){
return 0;
}
return steps.size();
}
...
}
Thanks for the hint. I got around this problem by replacing ActionBar with a custom ToolBar with back ImageButton and using click listener to get back to the calling activity.
backButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
getActivity().onBackPressed();
}
});

How to update custom RecyclerView from FragmentDialog?

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));
}
}

Using Fragments and Creating "Starting Page"

I'm new in Android App developing via Java. I'm using Eclipse. If I create an Activity, Eclipse automatically generates a Placeholderfragment Class and Fragment.xml. Can I disable this function? Or is it not advisable to do that? I delete those files because I find it more complicated to use than just write in one xml file at the moment.
Second question is how do I implement a "starting Page" for my App? For example some sort of a logopage which automatically disables after a few seconds and switches to a new activity. Create a separate Activity for it or do I use something else?
Actually you need two activities, one startup Activity which is used to show your logo or some guide,the other is a MainActivity which should be started by the startUp Activity.
In short You can do something like this:
public class MainActivity extends FragmentActivity {
Fragment fragment;
String className;
#Override
protected void onCreate(Bundle savedInstanceState) {
Log.d("MainActivity", "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Store the name of the class
className=MainActivity.class.getSimpleName();
//First fragment should be mounted on oncreate of main activity
if (savedInstanceState == null) {
/*fragment=FragmentOne.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.container, fragment).addToBackStack(className).commit();
*/
Fragment newFragment = FragmentOne.newInstance();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.container, newFragment).addToBackStack(null).commit();
Log.d("FRAGMENT-A", "fragment added to backstack");
}
}
}
FragmentOne.java
public class FragmentOne extends Fragment{
String className;
public static FragmentOne newInstance(){
Log.d("FragmentOne", "newInstance");
FragmentOne fragment = new FragmentOne();
return fragment;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d("FragmentOne", "onCreateView");
View view=inflater.inflate(R.layout.fragment_one, container, false);
//Store the name of the class
className=FragmentOne.class.getSimpleName();
return view;
}
}
Let me know if you need any more info
Well, in a Single Activity setup, the way I did this was the following:
public class SplashFragment extends Fragment implements View.OnClickListener
{
private volatile boolean showSplash = true;
private ReplaceWith activity_replaceWith;
private Button splashButton;
public SplashFragment()
{
super();
}
#Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
try
{
activity_replaceWith = (ReplaceWith) activity;
}
catch (ClassCastException e)
{
Log.e(getClass().getSimpleName(), "Activity of " + getClass().getSimpleName() + "must implement ReplaceWith interface!", e);
throw e;
}
startSwitcherThread();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.fragment_splash, container, false);
splashButton = (Button) rootView.findViewById(R.id.fragment_splash_button);
splashButton.setOnClickListener(this);
return rootView;
}
public void startSwitcherThread()
{
Thread splashDelay = new Thread()
{
#Override
public void run()
{
try
{
long millis = 0;
while (showSplash && millis < 4000)
{
sleep(100);
millis += 100;
}
showSplash = false;
switchToFirstScreen();
}
catch (Exception e)
{
e.printStackTrace();
}
}
};
splashDelay.start();
}
private void switchToFirstScreen()
{
activity_replaceWith.replaceWith(new FirstFragment());
}
#Override
public void onClick(View v)
{
if(v == splashButton)
{
if(showSplash == false)
{
switchToFirstScreen();
}
}
};
}
Where the ReplaceWith interface is the following:
import android.support.v4.app.Fragment;
public interface ReplaceWith
{
public void replaceWith(Fragment fragment);
}
And the replace function is implemented like so:
#Override
public void replaceWith(Fragment fragment)
{
getSupportFragmentManager()
.beginTransaction().replace(R.id.fragment_container, fragment)
.addToBackStack(null)
.commit();
}
Now, most people will say this is not a good approach if you're using multiple activities, and/or using multiple orientations and aren't just simply displaying a single Fragment in a single Activity no matter what. And they are completely right in saying so.
Multiple orientations would require the Activity to be responsible for knowing what is the "next" Fragment at a given replace call, and where to place it (which container, or to start it in a new Activity). So this is a valid approach only if you are certain that you only have one container and there is one Fragment shown at a given time.
So basically, if this does not apply to you, then you need to utilize the same approach (make a specific delay before you replace the current Fragment or Activity with another one, this specific code allows you that once the splash has been shown once, then clicking the button will automatically take you to the next screen - typical game splash setup, really), but use activity callbacks specific to the Fragment in order to swap one out for the other.
A Fragment setup I recommend and isn't relying on this special case can be seen here: Simple Android Project or its equivalent on Code Review: Fragment Start-Up Project

Categories