Here is a proposed solution that aims to limit the coupling of the fragment and the activity to the minimum.
The TabLayout is shown or hidden based on a Boolean LiveData object, hosted in a ViewModel shared by the activity and each fragment destination.
Each destination is responsible for setting the appropriate boolean value to the LiveData object, and the activity observes the LiveData object to show or hide the TabLayout.
To be able to use the TabLayout, the fragment declares an interface which the Activity must implement, and which contains a single method used to get a reference to the TabLayout.
Here are corresponding code samples, in Kotlin.
MainActivityViewModel
class MainActivityViewModel internal constructor() : ViewModel() {
val tabLayoutDestination = MutableLiveData<Boolean>()
fun setTabLayoutDestination(newValue : Boolean) {
//If the new value is the same, do not trigger an update
if (Objects.equals(tabLayoutDestination.value, newValue)) return
tabLayoutDestination.value = newValue
}
}
ExampleFragment
class ExampleFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var activityViewModel : MainActivityViewModel
private var tabLayout: TabLayout? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
...
val binding = FragmentExampleBinding.inflate(inflater, container, false)
val context = context ?: return binding.root
//Get the activity's ViewModel and specify that this destination requires a TabLayout
val mainViewModelFactory = InjectorUtils.provideMainActivityViewModelFactory()
activityViewModel = ViewModelProviders.of(activity!!, mainViewModelFactory)
.get(MainActivityViewModel::class.java)
activityViewModel.setTabLayoutDestination(true)
viewPager = binding.viewpager
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//If the parent activity implements the interface, get a reference to its TabLayout
val parentActivity = if(activity is TabLayoutHost) activity as TabLayoutHost else return
tabLayout = parentActivity.getTabLayoutReference()
//Setup the TabLayout and ViewPager
tabLayout?.setupWithViewPager(viewPager)
viewPager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
}
//The interface which must be implemented by the parent activity
interface TabLayoutHost {
fun getTabLayoutReference() : TabLayout
}
}
MainActivity
class MainActivity : AppCompatActivity(), ExampleFragment.TabLayoutHost {
private lateinit var activityViewModel : MainActivityViewModel
private lateinit var tabLayout: TabLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this,
R.layout.activity_main)
//Get the viewmodel
val mainViewModelFactory = InjectorUtils.provideMainActivityViewModelFactory()
activityViewModel = ViewModelProviders.of(this, mainViewModelFactory)
.get(MainActivityViewModel::class.java)
//Get a reference to the TabLayout
TabLayout = binding.tabLayout
//Subscribe to the boolean livedata, to be able to make
//the appropriate UI changes according to the fragment displayed
activityViewModel.tabLayoutDestination.observe(this, Observer {
if(it == true ) updateUiForTabLayoutDestination()
else updateUiForOtherDestination()
})
}
override fun getTabLayoutReference() = tabLayout
private fun updateUiForTabLayoutDestination() {
tabLayout.visibility = View.VISIBLE
}
private fun updateUiForOtherDestination() {
tabLayout.visibility = View.GONE
}
}
Those samples make use of several jetpack components (LiveData, ViewModel and Binding), and were derived from the sunflower sample app provided by Google.
The LiveData solution was inspired by this answer.
The example given here is for a TabLayout, but I've successfully applied it for other views (a progress bar and a fixed table header) by getting a reference to the whole AppBarLayout and calling findViewById to retrieve each view.