Android Best way to Communicate with fragments

Android: Best way to Communicate with fragments

Below are the two recommended options available to communicate between fragments and between fragment and host activity

  1. View Model
  2. Fragment Result API

The recommended option depends on the use case. To share persistent data with custom APIs, use a ViewModel. For a one-time result with data that can be placed in a Bundle, use the Fragment Result API.

Using View Model

ViewModel is an ideal choice when you need to share data between multiple fragments or between fragments and their host activity.

Share data with the host activity

In some cases, you might need to share data between fragments and their host activity.

Both your fragment and its host activity can retrieve a shared instance of a ViewModel with activity scope by passing the activity into the ViewModelProvider constructor. The ViewModelProvider handles instantiating the ViewModel or retrieving it if it already exists. Both components can observe and modify this data.

// View model
class MainViewModel : ViewModel() {
    private val mutableSelectedItem = MutableLiveData<Item>()

    val selectedItem: LiveData<Item> get() = mutableSelectedItem

    fun selectItem(item: Item) {
        mutableSelectedItem.value = item
    }
}

// Host activity
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.selectedItem.observe(this, Observer { item ->
            // Perform an action
        })
    }
}

// Fragment
class MainFragment : Fragment() {

    // viewmodel is scoped to host activity
    private val viewModel: MainViewModel by activityViewModels()

    // Called when the item is clicked.
    fun onItemClicked(item: Item) {
        // Set a new item.
        viewModel.selectItem(item)
    }
}

Share data between fragments

Two or more fragments in the same activity often need to communicate with each other.

These fragments can share a ViewModel using their activity scope to handle this communication. By sharing  ViewModel in this way, the fragments don’t need to know about each other, and the activity doesn’t need to do anything to facilitate the communication.

The following example shows how two fragments can use a shared ViewModel to communicate:


// View Model
class MainViewModel : ViewModel() {
    val filters = MutableLiveData<Set<Filter>>()

    private val originalList: LiveData<List<Item>>() = ...
    val filteredList: LiveData<List<Item>> = ...

    fun addFilter(filter: Filter) { ... }

    fun removeFilter(filter: Filter) { ... }
}

// Fragment
class ListFragment : Fragment() {
    
    // ViewModel in the activity scope.
    private val viewModel: ListViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
            // Perform action
        }
    }
}

// Fragment
class FilterFragment : Fragment() {

    // ViewModel in the activity scope.
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        viewModel.filters.observe(viewLifecycleOwner, Observer { set ->
            // Update the selected filters UI.
        }
    }

    fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter)

    fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter)
}

Both fragments use their host activity as the scope for the ViewModelProvider. Because the fragments use the same scope, they receive the same instance of the ViewModel, which enables them to communicate back and forth.

Share data between a parent and child fragment

When working with child fragments, your parent fragment and its child fragments might need to share data with each other. To share data between these fragments, use the parent fragment as the ViewModel scope, as shown in the following example:


class ListFragment: Fragment() {
    // Using the viewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel.
    private val viewModel: MainViewModel by viewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
            // Update the list UI.
        }
    }
}

class ChildFragment: Fragment() {
    // Using the viewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel using the parent fragment's scope
    private val viewModel: MainViewModel by viewModels({requireParentFragment()})
    ...
}

Scope a ViewModel to the Navigation Graph

If you’re using the Navigation library, you can also scope a ViewModel to the lifecycle of a destination’s NavBackStackEntry. For example, the ViewModel can be scoped to the NavBackStackEntry for the ListFragment:

class ListFragment: Fragment() {
    // Using the navGraphViewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel using the NavBackStackEntry scope.
    // R.id.list_fragment == the destination id of the ListFragment destination
    private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { item ->
            // Update the list UI.
        }
    }
}

 Fragment Result API

In Fragment version 1.3.0 and higher, each FragmentManager implements FragmentResultOwner. This means that a FragmentManager can act as a central store for fragment results. This change lets components communicate with each other by setting fragment results and listening for those results, without requiring those components to have direct references to each other.

Pass results between fragments

To pass data back to fragment A from fragment B, first set a result listener on fragment A, the fragment that receives the result. Call setFragmentResultListener() on fragment A’s FragmentManager, as shown in the following example:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResultListener("requestKey") { requestKey, bundle ->
        // We use a String here, but any type that can be put in a Bundle is supported.
        val result = bundle.getString("bundleKey")
        // Do something with the result.
    }
}

In fragment B, the fragment producing the result, set the result on the same FragmentManager by using the same requestKey. You can do so by using the setFragmentResult() API:

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

Fragment A then receives the result and executes the listener callback once the fragment is STARTED.

Pass results between parent and child fragments

To pass a result from a child fragment to a parent, use getChildFragmentManager() from the parent fragment instead of getParentFragmentManager() when calling setFragmentResultListener().

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Set the listener on the child fragmentManager.
    childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
        val result = bundle.getString("bundleKey")
        // Do something with the result.
    }
}

The child fragment sets the result on its FragmentManager. The parent then receives the result once the fragment is STARTED:

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

Receive results in the host activity

To receive a fragment result in the host activity, set a result listener on the fragment manager using getSupportFragmentManager().

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager
                .setFragmentResultListener("requestKey", this) { requestKey, bundle ->
            // We use a String here, but any type that can be put in a Bundle is supported.
            val result = bundle.getString("bundleKey")
            // Do something with the result.
        }
    }
}