Search code examples
androidandroid-fragmentsuinavigationcontrollerandroid-contextandroid-viewholder

Fragment not attached to host


Let me explain the whole thing, just in case. I'm using BottomNavigationView with Jetpack's Mobile Navigation graphs and so. I reimplemented the method:

NavigationUI.setupWithNavController(...)

during navigation setup, instead of using this method I made a very similar:

customSetupWithNavController(...)

the only changes I made was to change de default fading transition between fragments, and started to using more natural (in my opinion) side slide animations, just like this in onNavDestinationSelected:

if(origin == 0){
        builder.setEnterAnim(R.anim.slide_in_from_right)
                .setExitAnim(R.anim.slide_out_to_left)
                .setPopEnterAnim(R.anim.slide_in_from_left)
                .setPopExitAnim(R.anim.slide_out_to_right);
    }else if(origin == 1){
        builder.setEnterAnim(R.anim.slide_in_from_left)
                .setExitAnim(R.anim.slide_out_to_right)
                .setPopEnterAnim(R.anim.slide_in_from_right)
                .setPopExitAnim(R.anim.slide_out_to_left);
    }

Where origin stands for the direction where the incoming Fragment comes from.

It came with a problem: 2 of my fragments need a FAB to add elements to a recycler, and the side slide transitioning suddenly became ugly. So I added a single FAB in MainActivity's Layout, and a logic shows the FAB only when these 2 fragments are called.

I couldn't find a nice way to pass the click event from Activity to Fragments, because I wasn't able to instantiate the Fragments, since the Navigation handles the whole process.

So, what I did was to create a ViewHolder, since I know it can survive trough lifecycle changes. This ViewHolder holds an int, and a MutableLiveData, in the MainActivity logic I pass the current selected id of the element selected by the BottomNavigationView to the int, and only if the MainActivity's FAB is clicked the live Boolean is set to true. So, in Fragments onViewCreated() I added and observer to this Boolean, when the value is set to true, and the id passed to the ViewHolder matches with the id of the current fragment, the Boolean is set back to false, and something can be done, it's something like this:

eventsNotificationHandler.getClickEvent().observe(requireActivity(), new Observer<Boolean>() {
        @Override
        public void onChanged(Boolean aBoolean) {
            if(aBoolean && eventsNotificationHandler.getPositionId() == R.id.nav_contacts){
                eventsNotificationHandler.setClickEvent(false);
                //do something here
            }
        }
    });

This notificationHandler is the ViewHolder.

So far so good, at this point I can:

1- Navigate between BottomNavigationView' Fragments freely, and the FAB shows only for needed fragments.

2- Use Log.d(...) inside the observer any time I want, and see that the debug message just fine.

3- Toast a message, any time I want, ONLY if the context parameter was initialized outside the Observer, something like this:

Toast.makeText(previouslyDefinedContext, "SOME TEXT", Toast.LENGTH_SHORT).show();

What I can't:

1- Launch an Activity whenever I want, from inside the observer by using same idea than before, ONLY initializing the context before, and outside the Observer I was able to start the intent, just like this:

eventsNotificationHandler.getClickEvent().observe(requireActivity(), new Observer<Boolean>() {
        @Override
        public void onChanged(Boolean aBoolean) {
            if(aBoolean && eventsNotificationHandler.getPositionId() == R.id.nav_contacts){
                eventsNotificationHandler.setClickEvent(false);
                Intent newContact = new Intent(previouslyDefinedContext, NewContactActivity.class);
                startActivity(newContact);
                requireActivity().overridePendingTransition(R.anim.slide_in_from_right,R.anim.slide_out_to_left);
            }
        }
    });

But in this particular case I can launch the new Activity as many times as I want, BUT ONLY if I navigate directly to this particular fragment where the observer is defined after the app opens, if I decide to navigate first trough some other fragments instead, and then I go to this fragment to try to launch the Activity, the app crashes

I've noticed that this exact behavior happens when I call requireContext() from inside the Observer, it works but then stops working.

The app crashes with:

E/AndroidRuntime: FATAL EXCEPTION: main Process: cu.arrowtech.bpawreckproject, PID: 18019 java.lang.IllegalStateException: Fragment FragmentContacts{883259b} (9f127bdb-127d-4366-b90b-c8900a5a771e)} not attached to Activity

What I want:

1- A right way to launch a new Activity from inside a Fragment, by pressing a FAB in MainActivity, if it's possible.

2- A nice way to switch fragments if a possible solution implies to change the logic I have already.

3- Keep using Jetpack and Navigation Graphs.

I'm able to do what I want by using 2 separate FABs in each Fragment, but the transitioning is not nice and beautiful.

I'm open to suggestions, even if that implies to change the logic. I'm almost certain it must be a better way to do what I'm doing, instead of using ViewHolder for this purpose.

I would like to get something similar to the Google Pay, it seems to be that the Buttons for adding payment method, passes, and transfers is the same button, but it adapts to each situation.


Solution

  • After some reasearch I did found a way to keep fluid transitions between fragments AND Mobile Navigation components AND a single FAB in the MainActivity layout.

    What I did was to use an Interface instead of ViewModels (I always knew that approach was wrong):

    public interface SharedViewsInterface {
        void onFabClicked();
    }
    

    So in my MainActivity I defined that interface as null, and created a method to define it according to the situation. My MainActivity looks like:

    public class MainActivity extends AppCompatActivity {
        
        //UI elements
        private FloatingActionButton main_fab;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
            //Same dode to get the custom animations
            //.......
        
            //Main and only Fab configuration
            main_fab = findViewById(R.id.main_fab);
            main_fab.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(sharedViewsInterface !=null){
                        sharedViewsInterface.onFabClicked();
                    }
                }
            });    
        }
    
    
        //Other auxiliary methods
        public void setInterface(SharedViewsInterface sharedViewsInterface){
            this.sharedViewsInterface = sharedViewsInterface;
        }
    }
    

    By doing this, I can, from each fragment to implement the Interface in onCreate by doing this:

    ((MainActivity) requireActivity()).setInterface(new SharedViewsInterface() {
            @Override
            public void onFabClicked() {
                Intent newContact = new Intent(requireContext(), NewContactActivity.class);
                startActivity(newContact);
                requireActivity().overridePendingTransition(R.anim.slide_in_from_right_short,R.anim.slide_out_to_left_medium);
            }
    });
    

    This works well becouse the FAB is shown only when a fragment with Interface implementation is visible, see my example gif

    example app