35

I recently learned how to make nested fragments in Android. I don't know how communication is supposed to happen, though.

enter image description here

From reading the fragment communication documentation I know that

All Fragment-to-Fragment communication is done through the associated Activity. Two Fragments should never communicate directly.

This makes sense for sibling fragments within an activity, but it doesn't make as much sense for parent-child fragment communication. Do I need to go all the way up to the Activity just for the Child Fragment to talk to the Parent Fragment? If the answer is a simple "yes" then I can do that. If it is a "no", then what would the code design look like?

I see in the Nested Fragment documentation that one can use getParentFragment() to get a reference to the parent fragment. So does that mean that the child should directly communicate with the parent? That seems opposite from what is encouraged with a normal fragment communicating with the parent activity.

Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393

4 Answers4

50

Following Rahul Sharma's advice in the comments, I used interface callbacks to communicate up from the Child Fragment to the Parent Fragment and to the Activity. I also submitted this answer to Code Review. I am taking the non-answer there (at the time of this writing) to be a sign that there are no major problems with this design pattern. It seems to me to be consistent with the general guidance given in the official fragment communication docs.

Example project

The following example project expands the example given in the question. It has buttons that initiate upward communication from the fragments to the activity and from the Child Fragment to the Parent Fragment.

I set up the project layout like this:

enter image description here

Main Activity

The Activity implements the listeners from both fragments so that it can get messages from them.

Optional TODO: If the Activity wanted to initiate communication with the fragments, it could just get a direct reference to them and then call one of their public methods.

import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity implements ParentFragment.OnFragmentInteractionListener, ChildFragment.OnChildFragmentToActivityInteractionListener {

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

        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.parent_fragment_container, new ParentFragment());
        ft.commit();
    }

    @Override
    public void messageFromParentFragmentToActivity(String myString) {
        Log.i("TAG", myString);
    }

    @Override
    public void messageFromChildFragmentToActivity(String myString) {
        Log.i("TAG", myString);
    }
}

Parent Fragment

The Parent Fragment implements the listener from the Child Fragment so that it can receive messages from it.

Optional TODO: If the Parent Fragment wanted to initiate communication with the Child Fragment, it could just get a direct reference to it and then call one of its public methods.

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;


public class ParentFragment extends Fragment implements View.OnClickListener, ChildFragment.OnChildFragmentInteractionListener {


    // **************** start interesting part ************************

    private OnFragmentInteractionListener mListener;


    @Override
    public void onClick(View v) {
        mListener.messageFromParentFragmentToActivity("I am the parent fragment.");
    }

    @Override
    public void messageFromChildToParent(String myString) {
        Log.i("TAG", myString);
    }

    public interface OnFragmentInteractionListener {
        void messageFromParentFragmentToActivity(String myString);
    }

    // **************** end interesting part ************************



    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFragmentInteractionListener) {
            mListener = (OnFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_parent, container, false);
        view.findViewById(R.id.parent_fragment_button).setOnClickListener(this);
        return view;
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        Fragment childFragment = new ChildFragment();
        FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
        transaction.replace(R.id.child_fragment_container, childFragment).commit();
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

}

Child Fragment

The Child Fragment defines listener interfaces for both the Activity and for the Parent Fragment. If the Child Fragment only needed to communicate with one of them, then the other interface could be removed.

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;


public class ChildFragment extends Fragment implements View.OnClickListener {


    // **************** start interesting part ************************

    private OnChildFragmentToActivityInteractionListener mActivityListener;
    private OnChildFragmentInteractionListener mParentListener;

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.child_fragment_contact_activity_button:
                mActivityListener.messageFromChildFragmentToActivity("Hello, Activity. I am the child fragment.");
                break;
            case R.id.child_fragment_contact_parent_button:
                mParentListener.messageFromChildToParent("Hello, parent. I am your child.");
                break;
        }
    }

    public interface OnChildFragmentToActivityInteractionListener {
        void messageFromChildFragmentToActivity(String myString);
    }

    public interface OnChildFragmentInteractionListener {
        void messageFromChildToParent(String myString);
    }

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

        // check if Activity implements listener
        if (context instanceof OnChildFragmentToActivityInteractionListener) {
            mActivityListener = (OnChildFragmentToActivityInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnChildFragmentToActivityInteractionListener");
        }

        // check if parent Fragment implements listener
        if (getParentFragment() instanceof OnChildFragmentInteractionListener) {
            mParentListener = (OnChildFragmentInteractionListener) getParentFragment();
        } else {
            throw new RuntimeException("The parent fragment must implement OnChildFragmentInteractionListener");
        }
    }

    // **************** end interesting part ************************



    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_child, container, false);
        view.findViewById(R.id.child_fragment_contact_activity_button).setOnClickListener(this);
        view.findViewById(R.id.child_fragment_contact_parent_button).setOnClickListener(this);
        return view;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mActivityListener = null;
        mParentListener = null;
    }

}
Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • 2
    Great posts! I just want to add that your both "Optional TODO:" can be achieved by getting reference to Fragment or ChildFragment with methods: Activity.onAttachFragment(Fragment fragment) and Fragment.onAttachFragment(Fragment childFragment) – AppiDevo Jan 10 '17 at 20:09
  • @Suragch Couldn't be done by EventBus??? Your answer is perfect but don't you agree that this pattern adds more complexity to the app (in comparison with Event Bus ) – Milad Faridnia Dec 02 '17 at 13:00
  • @MiladFaridnia, Could be. I haven't used EventBus. I'd be interested in seeing an example of it if you would like to add an answer. – Suragch Dec 02 '17 at 14:18
  • @Suragch I've added the example. I've tested the code for passing data between fragments (parent and child fragment) and it works well. – Milad Faridnia Dec 03 '17 at 11:06
  • With this implementation you can not use the ChildFragment directly under any other Activity since `getParentFragment()` will return `null` in that case. For the sake of reusability I think communicating Fragments with their Activities is the best solution. Child Fragment invokes Activity, Activity invokes parent Fragment. Parent might implement `ChildFragment.OnChildFragmentInteractionListener`. We need to make more calls but it will not be limiting. What do you think? – Mustafa Berkay Mutlu Jun 01 '18 at 14:41
  • @MustafaBerkayMutlu, I haven't tried that before but it sounds like a valid option, especially if your child fragment might not always be a child fragment. Honestly, I've always found fragments confusing and difficult to work with. I currently prefer using custom views and view groups when possible. The communication patterns are similar. – Suragch Jun 01 '18 at 14:53
  • @MustafaBerkayMutlu For reusability you could consider a "bubble up" strategy where the onAttach() method first checks for an implementation by the parent fragment and then checks for implementation by the activity. The child fragment would only retain the first listener reference found instead of one for the parent fragment and one for the activity. This would allow either a parent fragment or an activity to contain the fragment and if necessary you can have the parentFragment continue to bubble up the event to the activity. – Allen Oliver Feb 06 '19 at 19:22
8

Although @Suragch's answer is correct, But I want to add another way to pass data between Fragments or Activity. No matter it's an Activity or Fragment you can pass data with event bus in 3 steps:

1- Define an event(message):

public class OrderMessage {
    private final long orderId;
    /* Additional fields if needed */
    public OrderMessage(long orderId) {
        this.orderId = orderId;
    }
    public long getOrderId() {
        return orderId;
    }
}

2- Register and Unregister for Events: To be able to receive events, the class must register/unregister for event bus. The best place for Activities and Fragments is onStart() and onStop()

@Override
public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
}

@Override
public void onStop() {
    EventBus.getDefault().unregister(this);
    super.onStop();
}

To be able to receive an event you have to subscribe to that event. To do that, add @Subscribe annotation to one of your methods in your class.

@Subscribe(threadMode = ThreadMode.MAIN)
   public void onMessage(OrderMessage message){
       /* Do something for example: */
       getContractDetails(message.getOrderId());
   }

3- Post an event

EventBus.getDefault().post(new OrderMessage(recievedDataFromWeb.getOrderId()));

More documentation and examples could be found Here. There are also other libraries like : Otto

Milad Faridnia
  • 9,113
  • 13
  • 65
  • 78
  • 1
    Sending events is not good way in this case. Because of two reason: 1- it will be optional not mandatory. So maybe some other user of this code forget to send or receive. 2- Flow readability is not so good. Let's think you meant define Event in child class because if you define any where else, you increased coupling of your code. – Mahdi Nov 20 '19 at 06:43
  • @Mahdi : Its just like the observer pattern that we use in live data. – Milad Faridnia Jan 14 '23 at 17:27
2

With the release of the architecture components you should probably have a look at the viewmodel architecture component.
In combination with live data you will be easily able to communicate between arbitrarily nested fragments. You might also to take a look at the todoapp and how they handle events there.

Vringar
  • 502
  • 1
  • 4
  • 14
2

Using ViewModel to communicate between nested Fragments

With pieces of the old ViewModel and LiveData now deprecated. I would like to provide information on this process as it currently stands.

In this example I am using the predefined TabLayout that can be selected when one creates a project.

Here I have a TextView in the parent fragment -- tab, with an EditText and a Button in the child fragment -- custom layout. It passes the text entered in the child fragment to the field in the parent fragment.

I hope this helps someone.

ViewModel Class

public class FragViewModel extends ViewModel {
  private MutableLiveData<CharSequence> digit = new MutableLiveData<>();

  public void insertDigit(CharSequence inDigit){
      digit.setValue(inDigit);
  }

  public LiveData<CharSequence> getDigit(){
      return digit;
  }
}

This is, for the most part, verbatim from the Android Developers ViewModel Overview with a small variable name change. The Overview was used as a guide for the parent and child code below.

The Parent Fragement Code

public class Tab0Main extends Fragment {

private FragViewModel model0;

// This is the Child Fragment.
private ChildFrag childFrag;

private TextView textView;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.tab0_main, container, false);

    // Setup the text field.
    textView = view.findViewById(R.id.tab0TextView);

    // Inserting the Child Fragment into the FrameLayout.
    childFrag = new ChildFrag();
    FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
    transaction.add(R.id.tab0Frame, childFrag).commit();

    return view;
}


@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    model0 = new ViewModelProvider(getActivity()).get(FragViewModel.class);
    model0.getDigit().observe(getViewLifecycleOwner(), new Observer<CharSequence>() {
        @Override
        public void onChanged(CharSequence charSequence) {
            textView.setText(charSequence);
        }
    });
}

The Child Fragment Code

public class ChildFrag extends Fragment {

private EditText fragEditText;
private Button fragButton;

// The ViewModel declaration
FragViewModel model;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.child_frag_layout, container, false);

    // The ViewModel Instantiation
    model = new ViewModelProvider(getActivity()).get(FragViewModel.class);

    fragEditText = view.findViewById(R.id.fragEditText);
    fragButton = view.findViewById(R.id.fragButton);
    fragButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            CharSequence in = fragEditText.getText();
            // Inserting a digit into the ViewModel carrier.
            model.insertDigit(in);
        }
    });
    return view;
  }
}

[![enter image description here][2]][2]