CategoriesNavigation

How to Communicate Between Fragments and Activity When Using Navigation Component

The android team introduced Navigation Component 2 years ago to encourage developers to use the single activity UI pattern. With the introduction of the navigation graph view in android studio, it provided a visually represented graph to work with fragments. Although it introduces a standard way to follow the single activity UI design pattern sometimes it can introduce new challenges to overcome. one of which is the way of communicating between navigation host activity and its fragments. The full project is added to Github.

Our Example

When using navigations architecture components your toolbar placed in the activity. As long as your application only required you to handle the back button and change the toolbar title, things are manageable. But in a case where you have a menu in the toolbar or having a search bar in the toolbar, you will need to send some data from activity to the current fragment and vice versa.

In our example, we have a navigation graph with two fragments. The first fragment has a summary card view which on user click it is going to direct the user to the second screen which contains a description view. In this description fragment, the user gets an option to set this article as a favorite using the menu option in the toolbar. Base on the user’s interaction toolbar favorite icon will be changed.

First, we need to find a way to add this menu to the toolbar when the user navigates to the description view. when the user comes back from that view this toolbar’s menu should be gone. In order to achieve this, we need to inflate the menu base on the current destination of the navigation graph.

private val navHostFragment by 
lazy { supportFragmentManager.findFragmentById(R.id.hostFragment) }

override fun onCreateOptionsMenu(menu: Menu): Boolean {    

	return when (navigationController.currentDestination?.id) {        
		R.id.fragmentDescription -> {            
			menuInflater.inflate(R.menu.favorite_menu, menu)                       
			true
		{
		else -> true
	}
}

To get this menu to visible in the toolbar we need to call invalidateOptionsMenu() when the second fragment created. Call that inside detail fragment onViewCreated() . As the next step, we need to remove the menu from the toolbar when the user returns the summary view. This can be achieved by overriding onSupportNavigateUp() in our activity. As we have used previously, based on the current destination of the navigation graph apply these changes to the toolbar menu.

Now the menu is displayed when the user in the correct destination in the navigation graph. Next, we need to handle the menu item click. This is the part where we need to figure out how to communicate from activity to the fragment. As the first step, we need to communicate to the fragment when the user clicks on the “favorite” or “non-favorite” icon. We can do this using an interface. Create an interface call MenuItemClickListener that has one function named fun onClickFavoriteMenuItem (markedFavorite: Boolean) . Then implement this interface in description fragment. In order to trigger this interface method from the activity, we need to get the current fragment from the navigation graph using childFragmentManager.

override fun onOptionsItemSelected(item: MenuItem): Boolean {

        val menuItemClickListener = navHostFragment?.childFragmentManager
            ?.fragments?.get(0) as MenuItemClickListener

        return when(item.itemId){
            R.id.mark_favorite ->{
                menuItemClickListener.onClickFavoriteMenuItem(false)
                true
            }
            R.id.mark_not_favorite ->{
                menuItemClickListener.onClickFavoriteMenuItem(true)
                true
            }
            else ->{
                super.onOptionsItemSelected(item)
            }
        }
}

So let’s pretend that fragment has to do some work and base on its result only the toolbar icon has to be changed. So in this example, I’ll change the menu Icon from “not-favorite” to “favorite”. After  onClickFavoriteMenuItem() function work is finished we need a way to communicate that result to the activity. To do That I’m using ReactiveX PublishSubject. I could use good all interface to do that, But It will make the fragment testing impossible in isolation ( using fragment scenario ) because in that way we will have to use a reference to the parent fragment.

class ActivityFunctionTrigger private constructor() {

    val favoriteStatusChangePublishSubject: PublishSubject<Boolean>
            = PublishSubject.create()

    private object HOLDER {
        val INSTANCE = ActivityFunctionTrigger()
    }

    companion object {
        val INSTANCE: ActivityFunctionTrigger by lazy { HOLDER.INSTANCE }
    }

    fun changeFavoriteStatus(markedFavorite: Boolean){
        favoriteStatusChangePublishSubject.onNext(markedFavorite)
    }
}

The above class contains the PublishSubject which our parent activity will be subscribed to. By calling changeFavoriteStatus() from the description fragment we can send the status of the operation. I created this class as a singleton in order to make the whole app share the same object of the class.

override fun onClickFavoriteMenuItem(markedFavorite: Boolean) {

        if(markedFavorite){
            Toast.makeText(context,"Marked favorite", Toast.LENGTH_SHORT).show()
        }else{
            Toast.makeText(context,"Marked non favorite", Toast.LENGTH_SHORT).show()
        }

        activityFunctionTrigger.changeFavoriteStatus(markedFavorite)

    }

Call changeFavoriteStatus() Inside previously overridden onClickFavoriteMenuItem() function in description fragment.

private var markedAsFavorite = false
private val activityFunctionTrigger by lazy { ActivityFunctionTrigger.INSTANCE }
private val compositeDisposable = CompositeDisposable()

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_example2)

        updateToolBarIcon()
}

private fun updateToolBarIcon(){

        val favoriteStatusChangeObserver = activityFunctionTrigger
            .favoriteStatusChangePublishSubject
            .subscribe {
                markedAsFavorite = it
                invalidateOptionsMenu()
            }

        compositeDisposable.add(favoriteStatusChangeObserver)
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {

        return when (navigationController.currentDestination?.id) {

            R.id.fragmentDescription -> {

                menuInflater.inflate(R.menu.favorite_menu, menu)

                val actionMarkFavorite = menu.findItem(R.id.mark_favorite)
                val actionMarkNonFavorite = menu.findItem(R.id.mark_not_favorite)

                if(markedAsFavorite){
                    actionMarkFavorite.isVisible = true
                    actionMarkNonFavorite.isVisible = false
                }else{
                    actionMarkFavorite.isVisible = false
                    actionMarkNonFavorite.isVisible = true
                }

                true
            }

            else -> true
        }
  }

override fun onDestroy() {
        compositeDisposable.dispose()
        super.onDestroy()
}

In our parent activity, updateToolBarIcon() function is used to subscribe to favoriteStatusChangePublishSubject . Base on its result markedAsFavorite variable value changes. The invalidateOptionsMenu() is called to re-draw the menu in the toolbar after that. onCreateOptionsMenu() function is modified to show the correct icon on the menu base on the current value of the markedAsFavorite variable. Finally favoriteStatusChangeObserver is disposed inside onDestroy() to prevent memory leaks.

Published by Chathuranga Shan

I am a mobile application developer who loves to get into new technologies in the Industry. Specialized in Android mobile application development with 5+ years’ experience working for different types of products across many business domains.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.