Multiple options menu showing up on app bar - android

Let me add more context:
I run bottom view navigation with a ViewPager2. For all 4 of my tab/fragments of my bottom navigation view I have an options menu which I created separately dynamically in each fragment.
Now, when we navigate through the app, it behaves correctly as every options menu is displayed ONLY for their respective fragment.
Problem is: Only when the app is launched, all the options menus from all the the 4 fragments show up on the start destination fragment's tab. BUT, once we swipe and swipe back, only the start destinations options menu is shown on the app bar. As is for every of the other 3 fragment/tabs.
Theory: I think it has something to do with onCreateOptionsMenu which is called when all four fragments are also created to which they share an app bar.
Is anybody familiar with this type of issue? Here is my PagerAdapters code for my ViewPager:
const val F1_PAGE_INDEX = 0
const val F2_PAGE_INDEX = 1
const val F3_PAGE_INDEX = 2
const val F4_PAGE_INDEX = 3
class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
/**
* Mapping of the ViewPager page indexes to their respective Fragments
*/
private val tabFragmentsCreators: Map<Int, () -> Fragment> = mapOf(
F1_PAGE_INDEX to { FirstFragment() },
F2_PAGE_INDEX to { SecondFragment() },
F3_PAGE_INDEX to { ThirdFragment() },
F4_PAGE_INDEX to { FourthFragment() }
)
override fun getItemCount() = tabFragmentsCreators.size
override fun createFragment(position: Int): Fragment {
return tabFragmentsCreators[position]?.invoke() ?: throw IndexOutOfBoundsException()
}
}
Here is also my Home View Pager Fragment where I create my bottom Navigation and affect to to the main fragments:
class HomeViewPagerFragment(): Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentViewPagerBinding.inflate(inflater, container, false)
val viewPager = binding.viewPager
viewPager.isUserInputEnabled = false
viewPager.adapter = PagerAdapter(this)
//Save states of four fragments
viewPager.offscreenPageLimit = 4
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
val bottomNavigation = binding.bottomNavView
bottomNavigation.setOnNavigationItemSelectedListener(
BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.fragment1_destination -> viewPager.currentItem = F1_PAGE_INDEX
R.id.fragment2_destination -> viewPager.currentItem = F2_PAGE_INDEX
R.id.fragment3_destination -> viewPager.currentItem = F3_PAGE_INDEX
R.id.fragment4_destination -> viewPager.currentItem = F4_PAGE_INDEX
}
true
})
return binding.root
}
}
The starting destination (first fragment) is where all the fragment's menu are shown at app start.
Any kind of help is appreciated! Thank you!

Issue: After endless hours, we have found a solution. The issue was directly created when we set the off screen page limit to 4. That causes all the fragments to be created at the same time, thus, obligates the option menus to be shown at launch on the starting destinations fragment since we instructed setHasOptionsMenu(true) in the onCreate of each fragment.
Solution: Simply, set the options menu to true in the onResumeof the fragment to only be called when we swipe to the respective fragment in this manner:
override fun onResume() {
super.onResume()
setHasOptionsMenu(true)
}

I think what you need to do is, make OptionsMenu visible only when that fragment is visible.
Try this: Put this in each fragment
override fun onResume(){
super.onResume()
setHasOptionsMenu(isVisible())
}
This will make the options menu visible only when that fragment is visible. You can make it hidden in onCreateView or in onPause if just onResume doesn't work.

Related

How to recreate or refresh fragment while swiping ViewPager2 tabs

I have a viewpager2 with FragmentStateAdapter() adapter inside it. I also have a tab layout with 4 tabs. I use a single fragment for all tab. that is named AllOrdersTab.
in my architecture I just send different value to load different API data to AllOrdersTab fragment.
when each tab layout is selected , a fragment created and works fine for the first time for all 4 fragments. after that if I swipe back to previous tab it is not created or refreshed again.
I want to recreate the fragment or a way to call API again when swiping between tabs. I also read this page.
FragmentStateAdapter not recreating currentFragment after notifyDataSetChanged
I tried to do this. so I decided to create 4 instance of Allorderstab() fragment. but never work for me because I guess hash codes of fragments are same.
ViewPager2 Adapter:
class ViewPagerOrdersAdapter(fm: FragmentManager,val listFragments:MutableList<Fragment>, viewlifecycler: Lifecycle) : FragmentStateAdapter(fm, viewlifecycler)
{
override fun getItemCount(): Int
{
return listFragments.size
}
override fun createFragment(position: Int): Fragment {
val args = Bundle()
when (position) {
1 -> {
args.putString("KEY_ID", "inProgress")
listFragments[position].arguments = args
return listFragments[position]
}
2 -> {
args.putString("KEY_ID", "cancel")
listFragments[position].arguments = args
return listFragments[position]
}
3 ->
{
args.putString("KEY_ID","deliver")
listFragments[position].arguments=args
return listFragments[position]
}
else -> {
args.putString("KEY_ID", "all")
listFragments[position].arguments = args
return listFragments[position]
}
}
}
override fun getItemId(position: Int): Long {
return listFragments[position].hashCode().toLong()
}
override fun containsItem(itemId: Long): Boolean {
return listFragments.find {it.id.hashCode().toLong() == itemId } != null
}
}
Here I created 4 instance of Allorderstab() Fragment.
Set ViewPager2 Adapter
MainFragment:
val fragments:MutableList<Fragment> = mutableListOf(Allorderstab(), Allorderstab(), Allorderstab(), Allorderstab())
vp.setAdapter(ViewPagerOrdersAdapter(this.childFragmentManager,fragments, lifecycle))
AllOrderstab Fragment:
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?, savedInstanceState: Bundle?): View? {
val bundle = arguments
bundle?.let {
val myStatus = bundle.getString("KEY_ID")
myStatus?.let{
//getting history of each tab orders - calling API
myviewModel.getOrdersHistory(tempTn,myStatus)
}
}
}
All in all I don't know how to refresh while swiping between tabs . if I have a single fragment for all 4 tabs.
For those who wasted a day for this problem like me , wanting to call API each time for refreshing data, Move your code to onResume function. fortunately it is run each time your fragment visible.
AllOrderstab Fragment:
override fun onResume() {
val bundle = arguments
bundle?.let {
val myStatus = bundle.getString("KEY_ID")
myStatus?.let{
//getting history of all orders
myviewModel.getOrdersHistory(tempTn,myStatus)
}
}
super.onResume()
}
You have 4 different instances of the same tab. If you need to refresh the tabs everytime the user navigates to your tab you need to add a PageChangeListener to your tabs view. Then whenever you change the page you need to notify the fragment that it has come to foreground you can do so by calling a method on Allorderstab class and then refreshing the data from this method.

OnResume app becomes blank in Kotlin but works fine in Android Java

I am migrating my app from Java to Kotlin. I have generated app with default Navigation drawer and renamed the Fragments associated with navigation drawer items.
These Fragments (Home, Settings, Notification) contains some UI elements and one of them, HomeFragment contains a viewpager with five(5) views in it. I have implemented everything correctly and everything following this tutorial which works fine until I resume the app.
As soon as I press the home button and resume the app, I get blank screen but I can see actionbar/ toolbar properly.
Pager adapter
class PagerAdapter(manager: FragmentManager):FragmentStatePagerAdapter(manager,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT){
override fun getCount(): Int {
return 5;
}
override fun getItem(position: Int): Fragment {
return when (position) {
0 -> OneFragment.newInstance()
1 -> TwoFragment.newInstance()
2 -> ThreeFragment.newInstance()
3 -> FourFragment.newInstance()
4 -> FiveFragment.newInstance()
else -> OneFragment.newInstance()
}
}
override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) {
super.setPrimaryItem(container, position, `object`)
}
}
This is where I am attaching this viewpager in the HomeFragment. I have created the viewbind class following this tutorial.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val pagerAdapter = PagerAdapter(childFragmentManager)
binding?.homePager?.adapter = pagerAdapter
}
I also get a lot of warnings -
ResourceType: Too many attribute references
RecyclerView: No adapter attached; skipping layout
Skipping 55 frames
This app literally with just default code generated by android. except one recyclerview. No heavy UI operation going on becuase I am just passing a predefined string array in recyclerviewadapter.

Button lose listener after fragment replace

I have the weirdest bug on Kotlin, and after two days of trying I finally asking for help.
The problem is simple : I have two fragment and one activity, the first fragment A is a form, with a validate button, when I click on validate, the fragment B replace the fragment A, and if I press back, the fragment A show up again with the form filled.
My problem is that after the fragment is shown again, I can click on the button but the listener is not call, so I can't go to the fragment B again. The strange thing is that the other listener are properly working, so I'm thinking it's because the previous fragment is catching the onClick, but idk what to do. Here is some code :
ViewUtils :
fun addFragment(activity: Activity, fragment: androidx.fragment.app.Fragment, container: Int) {
val fragmentManager = (activity as AppCompatActivity).supportFragmentManager
val pendingTransaction = fragmentManager.beginTransaction().add(container, fragment, fragment.javaClass.name)
pendingTransaction.commitAllowingStateLoss()
}
fun replaceFragment(manager: FragmentManager, fragment: androidx.fragment.app.Fragment, container: Int) {
if (fragment.isAdded) return
val pendingTransaction = mangaer.beginTransaction()
pendingTransaction.replace(container, fragment, fragment.javaClass.name)
pendingTransaction.commitAllowingStateLoss()
}
fun removeFragment(activity: Activity, fragment: Fragment) {
val manager = (activity as AppCompatActivity).supportFragmentManager
val trans = manager.beginTransaction()
trans.remove(fragment)
trans.commit()
manager.popBackStack()
}
Activity :
fun displayFragmentA() {
ViewUtils.replaceFragment(supportFragmentManager, FragmentA,
R.id.fragmentLayout)
}
fun FragmentB() {
ViewUtils.replaceFragment(supportFragmentManager, FragmentB,
R.id.fragmentLayout)
}
Fragment A
class AFragment : BaseFragment(), AContract.View {
companion object {
#JvmStatic
fun newInstance(): AFragment {
val fragment = AFragment()
return fragment
}
}
#Inject
lateinit var APresenter: AContract.Presenter<AContract.View>
//end region
//region lifecycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_A_layout, container, false)
}
override fun onResume() {
super.onResume()
button_validate.setOnClickListener {
presenter.goToNextStep()
}
}
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
The listener was set in the onViewCreated but I tried moving it to onResume (didn't change anything)
Fragment B code is not important I think, but I can add it if it helps.
Any help is welcome, I really don't know what's going on, the replace/add methods were there before I came to the project, they are not perfect but they are working elsewhere on the project.
I try using breakpoint, the button is not null but we never enter the listener.
Edit : I tried on 3 differents devices, I don't have the bug with a Sony Android 9, but with Huawei et One plus 6 Android 10, the problem persist ..
Ok so after asking to a lot of people, the only solution I found is not using kotlin.synthetic, and using findById instead :
view.findViewById<Button>(R.id.button_validate.setOnClickListener

Hide Bottom Navigation View in fragment

I want to hide bottomNavigationView in some fragments.
I have tried the below code, but it has a flicker effect. (bottomNavigationView hide before the nextFragment becomes visible.
val navController = this.findNavController(R.id.nav_host_home)
navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.searchArticlesFragment -> bnvMain.visibility = View.GONE
R.id.articleFragment -> bnvMain.visibility = View.GONE
else -> bnvMain.visibility = View.VISIBLE
}
}
I have also tried another code. But it resizes the fragment. And giving OutOfMemoryException in Destination Fragment.
supportFragmentManager.registerFragmentLifecycleCallbacks(object :
FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(
fm: FragmentManager,
f: Fragment,
v: View,
savedInstanceState: Bundle?
) {
when (f) {
is SearchArticlesFragment -> bnvMain.visibility = View.GONE
is ArticleDetailsFragment -> bnvMain.visibility = View.GONE
else -> bnvMain.visibility = View.VISIBLE
}
}
}, true)
Please help me how can I hide the bottomNavigationView in the proper and best possible way? Is this the only way I can hide the bottomNavigationView? How youtube and Instagram achieve this behavior?
If your code follows single activity design pattern then the following solution suites you.
Create a method inside the parent activity to hide/show bottomNavigationView.
Create a BaseFragment class(create your fragments by extending this BaseFragment Class)
In the BaseFragment create a variable to hold the bottomNavigationViewVisibility (hide/show)
In onActivityCreated method of the BaseFragment, get the activity reference and set the bottomNavigationViewVisibility by calling the method which we created in STEP1.
In each fragment you create, just set the bottomNavigationViewVisibility variable.
Example:
In parentAcitivty layout, file add bottomNavigationView
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/main_bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
app:labelVisibilityMode="labeled"
app:menu="#menu/main_nav" />
Step 1: In parent activity, create a method to change the visibility.
fun setBottomNavigationVisibility(visibility: Int) {
// get the reference of the bottomNavigationView and set the visibility.
activityMainBinding.mainBottomNavigationView.visibility = visibility
}
Step 2 & 3 & 4:
abstract class BaseFragment : Fragment() {
protected open var bottomNavigationViewVisibility = View.VISIBLE
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// get the reference of the parent activity and call the setBottomNavigationVisibility method.
if (activity is MainActivity) {
var mainActivity = activity as MainActivity
mainActivity.setBottomNavigationVisibility(bottomNavigationViewVisibility)
}
}
override fun onResume() {
super.onResume()
if (activity is MainActivity) {
mainActivity.setBottomNavigationVisibility(bottomNavigationViewVisibility)
}
}
}
Step 5:
class SampleFragment1 : BaseFragment() {
// set the visibility here, it takes care of setting the bottomNavigationView.
override var navigationVisibility = View.VISIBLE
// override var navigationVisibility = View.GONE
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_sampleFragment1, container, false)
}
}

Splash fragment wont display when using button.performClick()

edit: 2020.4.12 correct typo from Button.performClick() to button.performClick()
I am writing an app which should display a splash page/fragment for a
few seconds at start then display the next fragment in the navgraph. There are seven fragments in the navgraph which I can navigate around those fragments just fine.
The issue is with the splash fragment, I can only get the splash fragment to display/inflate when the button.onClickListener is set to accept a manual user
input -> click. (vs using button.performClick())
The desired end result is to display a fragment layout consisting of an image view and a text view for a few seconds at app start before displaying the next fragment layout in the navgraph, without having the user to click or press anything.
I have tried using threadsleep, a runnable with a handler, and even a while loop with performClick(). None of which have yielded acceptable results. The closest I have come to getting the desired result is the following:
class SplashFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater,
rootContainer: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflater(R.layout.fragment_splash, rootContainer, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
updateABode()
}
// pizzaLoop initialized to give about 4 seconds delay
val pizzaLoop = 1500000000
while (pizzaLoop > 0) {
pizzaLoop--
if (pizzaLoop == 0) {
button.performClick()
}
}
private fun updateABode {
val ABode = "A" // hard coded for testing purposes
when (ABode) {
"B" -> // for testing purposes only -- does nothing
"A" -> findNavController().navigate(R.id.action_splashFragment_to_firmwareFragment)
}
}
}
With the pizzaLoop installed, the splash fragment will not inflate, but I do see the delay via the firmware screen update. (intially all I get is a white blank screen then subsequent calls to the SplashFragment class show nothing but the firmwareFragment screen (next in the navgraph) -- and the pizzaLoop delay is noticable).
When I comment out the pizzaLoop then the splash fragment displays as intended but I have to click the button to bring up the next fragment in the navgraph (the rest of the navgraph works fine).
It's like the button.performClick() method is preventing the inflation of the splash fragment.
EDIT: 2020.4.12 TO PROPERLY POST SOLUTION.
class SplashFragment : Fragment() {
private val handler: Handler = Handler()
private val updateRunnable: Runnable = Runnable { updateABode() }
override fun onCreateView(inflater: LayoutInflater,
rootContainer: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflater(R.layout.fragment_splash, rootContainer, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
handler.removeCallbacks(updateRunnable)
}
handler.postDelayed(updateRunnable, 4000)
}
private fun updateABode {
val ABode = "A" // hard coded for testing purposes
when (ABode) {
"B" -> // for testing purposes only -- does nothing
"A" -> findNavController().navigate(R.id.action_splashFragment_to_firmwareFragment)
}
}
}

Categories

Resources