15 minutes read

Sometimes users who just start using fragments have many questions about a fragment's lifecycle. And how to transfer data between fragments themselves or between fragments and activity? This is precisely what communicating to activity and result listener are for.

Fragment lifecycle methods

The lifecycle of a fragment is slightly different from that of an activity since the fragment itself is completely dependent on the activity and doesn't exist on its own. So, many methods are either named the same as activity methods or have very similar names:

  • onAttach() — the method called when the fragment is attached to the activity.

  • onCreate() — the method called when a fragment is created.

  • onCreateView() — the method which creates and returns the view hierarchy associated with the fragment. You can use it to inflate your layout.

  • onViewCreated() — the method called when the views have been created and attached. All views must be initialized here.

class YourFragmentName : Fragment() {
    private var someFirstView: SomeView? = null
    private var someSecondView: SomeView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        someFirstView = view.findViewById(R.id.firstViewId)
        someSecondView = view.findViewById(R.id.secondViewId)
    }
}
  • onActivityCreated() — the method called after the Activity.onCreate() method.

  • onStart() — the method called when the fragment becomes visible to the user (see the Activity.onStart() method).

  • onResume() — the method called before the fragment becomes available for user interaction (see the Activity.onResume() method).

  • onPause() — the user doesn't interact with the fragment but part of the fragment is visible to the user.

  • onStop() — the method that makes the fragment invisible to the user.

  • onSaveInstanceState() — the method that saves the fragment state.

  • onDestroyView() – the fragment clears the resources associated with the view hierarchy. Here you have to null all your views because holding destroyed views can cause a memory leak.

override fun onDestroyView() {
    super.onDestroyView()
    someFirstView = null
    someSecondView = null
}
  • onDestroy() – the method called before the fragment is destroyed.

  • onDetach() – method called before the fragment is detached from the activity.

Fragment and activity lifecycle are very similar but have minor differences. First and foremost, a fragment can't exist without an activity. This means that the activity will always be created first.

Also, if you remove a fragment using the FragmentTransaction remove() or replace() method and add it to the back stack, the onDestroyView() method will still be called but the onDestroy() method will not. As for onCreateView(), onViewCreated() and onStart(), they are called immediately when returning. If not added to the back stack, the fragment will be destroyed using the onDestroy() method.

Communicating to activity

Sometimes the data from the fragment needs to be transferred to the activity. For this, it is best to create an interface in a fragment and subsequently implement it in the desired activity.

In order to pass something to an activity, first, you need to create an interface in the desired fragment.

class YourFragment : Fragment() {

    override fun onCreateView(...) { ... }

    interface YourFragmentListener {
        fun doSomething()
    }
}

Next, you need to implement your interface in the desired activity.

class MainAсtivity : AppCompatActivity(), YourFragment.YourFragmentListener {

    ...

    override fun doSomething() {
        Log.d("Log", "Doing something!")
    }
}

When you are implementing your interface, be sure to include the fragment name before the interface name: YourFragment.YourFragmentListener.

The penultimate step is to get an instance of the Activity implementing your interface in the Fragment.

    private var callback: YourFragmentListener? = null
    override fun onAttach(context: Context) {
        super.onAttach(context)
        callback = context as YourFragmentListener
    }
    override fun onDetach() {
        super.onDetach()
        callback = null
    }

Finally, you only need to call the interface instance at the location you want, such as the onClick() button.

Result listener

Android has a very useful API for passing data between fragments — the Fragment Result API. It's a tool that makes it possible to pass data between fragments using a user-specified key. All this happens with a FragmentManagerparentFragmentManager and childFragmentManager. So, for starters, it’s worth figuring out which manager to use in your case since it's the only thing that makes a difference.

If your fragments are independent of each other, it's a good idea to use parentFragmentManager.

Independent fragments relations diagram

In cases when one fragment is a child fragment, the parent fragment must use childFragmentManager, and the child fragment must use parentFragmentManager.

nested fragments relations diagram

After selecting the desired FragmentManager in the fragment which will send data, call the setFragmentResult() method. In this method, set the result in the FragmentManager identical to the first one and use the same key. The parameters of this method are the key itself and the Bundle object that needs to be passed to another fragment.

button.setOnClickListener {
    parentFragmentManager.setFragmentResult("key", bundleOf("bundleKey" to yourObject))
}

Next, in the onCreate() method of the fragment where you want to receive your data, you need to call the setFragmentResultListener() method. The first parameter of this method is the key parameter: it must be the same as the key you specified in the other fragment.

YourFragmentManager.setFragmentResultListener("key", this) { key, bundle ->
    val result = bundle.get("bundleKey")
}

The received data can, for example, be output to the log or displayed to the user. This can be a user profile similar to the one shown in the example below:

Here is the first fragment with an empty profile, and then a completed profile after entering information in the next fragment:

class DefaultFragment : Fragment() {
    private var enterData: Button? = null
    private var showName: TextView? = null
    private var showAge: TextView? = null
    private var showUniversity: TextView? = null

    private var name: String? = null
    private var age: String? = null
    private var university: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        parentFragmentManager.setFragmentResultListener("requestKey", this) { key, bundle ->
            name = bundle.getString("Name")
            age = bundle.getString("Age")
            university = bundle.getString("University")

            showName?.text = name
            showAge?.text = age
            showUniversity?.text = university
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_default, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        enterData = view.findViewById(R.id.enterData)
        showName = view.findViewById(R.id.showName)
        showAge = view.findViewById(R.id.showAge)
        showUniversity = view.findViewById(R.id.showUniversity)

        enterData!!.setOnClickListener {
            parentFragmentManager
                .beginTransaction()
                .replace(R.id.container, InputFragment())
                .addToBackStack(null)
                .commit()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        enterDate = null
        showName = null
        showAge = null
        showUniversity = null
    }
}

Here is the second fragment where the user enters data:

class InputFragment : Fragment() {

    private var sendData: Button? = null
    private var enterName: EditText? = null
    private var enterAge: EditText? = null
    private var enterUniversity: EditText? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_first, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        sendData = view.findViewById(R.id.sendData)
        enterName = view.findViewById(R.id.nameEditText)
        enterAge = view.findViewById(R.id.ageEditText)
        enterUniversity = view.findViewById(R.id.universityEditText)

        sendData!!.setOnClickListener {
            parentFragmentManager.setFragmentResult(
                "requestKey",
                bundleOf(
                    "Name" to enterName?.text.toString(),
                    "Age" to enterAge?.text.toString(),
                    "University" to enterUniversity?.text.toString(),
                )
            )
            parentFragmentManager.popBackStack()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        sendData = null
        enterName = null
        enterAge = null
        enterUniversity = null
    }
}

The fragment accepts all values passed to it and displays them on the screen:

fragments changes with given data

Conclusion

Fragment and Activity are very similar in their purpose, but they have quite a few differences in their lifecycle and their use. When you use the Fragment Result API, be sure that you choose the right fragment manager for it to work as intended, and always check that the key is correct, otherwise data can't be retrieved

20 learners liked this piece of theory. 2 didn't like it. What about you?
Report a typo