App's navigation happens through a BottomNavigationView.
On the 1st position, I have a fragment which has button to navigate to another fragment.The first time it loads and shows everything. The, I click on 1st position on BotttomNavView again, we going back to parent fragment (bound to 1st position of BottomNavigationView). But from this point, if I click the button and open the fragment, it is always blank.
Here is how my fragment management looks like:
fun navigateTo(
context: Context,
fragment: BaseFragment,
navigatable: Navigatable,
addToBackStack: Boolean
) {
val activity = ContextUtil.getActivityFromContext(context)
val tag = fragment.javaClass.toString()
val fragmentManager = (activity as BaseActivity).supportFragmentManager
val transaction = fragmentManager.beginTransaction()
if (fragmentManager.findFragmentByTag(fragment.javaClass.toString()) == null) {
transaction.add(R.id.container, fragment, tag).hide(fragment)
}
val activeFragment = findVisibleFragment(context)
transaction
.apply {
if (activeFragment != null) {
hide(activeFragment)
}
}
.show(fragment)
.apply {
if (addToBackStack) {
this.addToBackStack(tag)
}
}
.commit()
navigatable.afterScreenTransition(fragment).invoke()
}
I did an experiment and found that onAttach() gets called in my pager fragment, but all the views are null, onCreateView() not called.
So after further experimenting, I did supportFragmentManager.popBackStack() before opening the 1st fragment. Now opening the child fragment is not blank.
Related
Is there a way to navigate from a child fragment of ViewPager2 to another fragment?
Let's say I have a parent fragment [ParentFragment] containing only a ViewPager2 and a TabLayout. I have 2 children fragments [ChildrenFragment1 and ChildrenFragment2] created with FragmentStateAdapter. I want to navigate from ChildrenFragment2 to another FragmentB so that when i navigate back from FragmentB, to show position of the ParentFragment to ChildFragment2?
ex: ChildFragment1 -> ChildFragment2 -> FragmentB -> ChildFragment2
I get:
java.lang.IllegalArgumentException: Navigation action/destination id:actionChildFragment2_to_FragmentB cannot be found from the current destination
I found a solution, i keep the current page in a ViewModel LiveData property, setting it on page change inside the ParentFragment:
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(page: Int) {
viewModel.updatePageSelected(page)
}
})
and observe the page change inside the ParentFragment:
subscribeToLiveData(viewModel.viewPagerSelected) { page ->
binding.viewPager.setCurrentItem(page, false)
}
The ParentViewModel:
private val _viewPagerSelected = MutableLiveData<Int>()
val viewPagerSelected: LiveData<Int> = _viewPagerSelected
fun updatePageSelected(newSelection: Int) {
_viewPagerSelected.value = newSelection
}
I have a bottom nav with 4 fragments Home, Following, Notification, and Profile, there is no issue with the bottom navigation on backstack , but now for eg from profile fragment I jumped to a fragment called edit_profile which is not a part of the bottom nav and when press back I want that it should go back to the profile fragment but the backstack is taking me from edit_profile to directly home fragment
here is a recording link
I recently change my project from java to kotlin and I'm a beginner in kotlin
i really like the navigation of Pinterest and Instagram
Note:- All this code is automatically changed to kotlin (with some
changes done manually ) , this issue was also with java and not after migrating to kotlin , Also if you want more reference of the code
please tell me i will update the question
Code
MainActivity.kt // Bottom Nav
class MainActivity : AppCompatActivity() {
var bottomNavigationView: BottomNavigationView? = null
var integerDeque: Deque<Int> = ArrayDeque(3)
var flag = true
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
setContentView(R.layout.activity_main)
val window = this.window
window.statusBarColor = this.resources.getColor(R.color.black)
bottomNavigationView = findViewById(R.id.bottom_navigation_view)
integerDeque.push(R.id.nav_home)
loadFragments(Home_Fragment())
bottomNavigationView!!.selectedItemId = R.id.nav_home
bottomNavigationView!!.setOnNavigationItemSelectedListener(
BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem ->
val id = item.itemId
if (integerDeque.contains(id)) {
if (id == R.id.nav_home) {
integerDeque.size
if (flag) {
integerDeque.addFirst(R.id.nav_home)
flag = false
}
}
integerDeque.remove(id)
}
integerDeque.push(id)
loadFragments(getFragment(item.itemId))
false
}
)
}
#SuppressLint("NonConstantResourceId")
private fun getFragment(itemId: Int): Fragment {
when (itemId) {
R.id.nav_home -> {
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
R.id.nav_following -> {
bottomNavigationView!!.menu.getItem(1).isChecked = true
return Following_Fragment()
}
R.id.nav_notification -> {
bottomNavigationView!!.menu.getItem(2).isChecked = true
return Notification_Fragment()
}
R.id.nav_profile -> {
bottomNavigationView!!.menu.getItem(3).isChecked = true
return Profile_Fragment()
}
}
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
private fun loadFragments(fragment: Fragment?) {
if (fragment != null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment, fragment.javaClass.simpleName)
.commit()
}
}
override fun onBackPressed() {
integerDeque.pop()
if (!integerDeque.isEmpty()) {
loadFragments(getFragment(integerDeque.peek()))
} else {
finish()
}
}
Edit_Profile.kt // from this fragment i want to go back to the last fragment which should be the profile fragment
class Edit_Profile : Fragment() {
private var profilePhoto: CircleImageView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_edit_profile, container, false)
profilePhoto = view.findViewById(R.id.circleImageView)
initImageLoader()
setProfileImage()
val imageView = view.findViewById<ImageView>(R.id.backArrow)
imageView.setOnClickListener {
val newCase: Fragment = Profile_Fragment()
assert(fragmentManager != null)
val transaction = requireFragmentManager().beginTransaction()
transaction.replace(R.id.fragment_container, newCase)
transaction.addToBackStack(Profile_Fragment.toString())
transaction.commit()
}
return view
}
Edit
added a part of the transaction from Profile Fragment to Edit Profile
ProfileFragment.kt
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
})
Now you are managing the back stack through the integerDeque array.
When you go to a new BottomNavigationView fragment; you added its id to the array if it doesn't already exist.
When you pop up the back stack; the fragment at the top is kicked off the array.
But since you pushed all those ids in the bottomNavigationView.setOnItemSelectedListener callback; then the integerDeque array only contains BottomNavigationView fragments ids.
And as the Edit_Profile fragment is not a part of BottomNavigationView fragments, then it won't be added/popped off the queue. Instead when you try to popup the back stack whenever the Edit_Profile fragment is shown; the normal behavior you manage in the onBackPressed() continues and the Profile_Fragment id will pop up from the queue making you return to the preceding fragment (Home_Fragment) in your mentioned example.
A little fix to this is to consider adding an id into the queue when you transact to Edit_Profile fragment so that this id is popped off the queue resulting in back to Profile_Fragment fragment.
You can do that with the fragment's id in order to make sure it's unique:
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
(requireActivity() as MainActivity).integerDeque.push(id) // <<<< pushing id to the queue
})
This should fix your problem.
Side tips:
Use setOnItemSelectedListener instead of setOnNavigationItemSelectedListener on the BNV as the latter is deprecated.
Return true instead of false from setOnItemSelectedListener callback as this should consume the event and mark the BNV as selected.
In Edit_Profile transaction replace the fragment instead of adding it with add as already the container is consumed; and this would make you avoid overlapping fragments in the container.
In onBackPressed(); you'd replace loadFragments(..) with bottomNavigationView.selectedItemId = integerDeque.peek(); this could be lighter to reuse the same fragment instead of redoing the transaction.
Usually I follow this pattern
Where I add HomeF in main container which includes all bottom nav tab, and all bottom nav tab will open in home container, and those fragment which are not part of bottom nav will open in main container. I generally add(not replace) all the fragments in main container and set add to back stack , so that if user goes from profile (home_container) to something in main container , while backstack we can pop the top fragment and user will be seeing profile.
According to the Android docs:
replace() is essentially the same as calling remove(Fragment) for all
currently added fragments that were added with the same
containerViewId and then add(int, Fragment, String) with the same
arguments given here.
However my code says otherwise.
My app has 1 activity and multiple fragments. It also has a BottomNavigationView with 3 tabs (Options, Game, Leaderboards).
When the MainActivity is initialised, 3 fragments are added to the container FrameLayout MainActivity. Immediately after being added, 2 fragments are hidden which leaves 1 shown on the screen (the opening fragment).
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.fragment_layout, firstFragment, "Opening")
transaction.add(R.id.fragment_layout, OptionsFragment(), "Options")
transaction.add(R.id.fragment_layout,LeaderboardsFragment(), "Leaderboards")
transaction.commitNow()
val transaction2 = supportFragmentManager.beginTransaction()
val options: androidx.fragment.app.Fragment = supportFragmentManager.findFragmentByTag("Options")!!
val leaderboards: androidx.fragment.app.Fragment = supportFragmentManager.findFragmentByTag("Leaderboards")!!
transaction2.hide(options)
transaction2.hide(leaderboards)
transaction2.commitNow()
}
From here on out, each fragment that isn't OptionsFragment() or LeaderboardsFragment() is swapped out via replace().
OpeningFragment.kt
transaction?.replace(R.id.fragment_layout, playerDetailsFragment, "Add Player")
transaction?.commit()
gameString = "Add Player"
OptionsFragment() and LeaderboardsFragment() are put into view by being selected from the bottom nav, which then hides every other fragment except for the one selected (e.g if Options is selected from bottom nav, then every fragment is hidden except for OptionsFragment()).
MainActivity.kt
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val fragmentTags = arrayListOf("Opening", "Leaderboards", "Options", "Add Player", "Question", "Location", "Game Over")
val transaction = supportFragmentManager.beginTransaction()
val selectedFragmentTag = when (item.itemId) {
R.id.action_leaderboards -> "Leaderboards"
R.id.action_options -> "Options"
else -> {
gameString
}
}
// Selected Fragment
val selectedFragment: androidx.fragment.app.Fragment? = supportFragmentManager.findFragmentByTag(selectedFragmentTag)
for (fragment_tag in fragmentTags){
// Hide every Fragment that has been added and isn't the selected Fragment
val fragment = supportFragmentManager.findFragmentByTag(fragment_tag)
if (fragment != null && fragment != selectedFragment) {
transaction.hide(fragment)
}
}
transaction.show(selectedFragment!!)
transaction.commit()
return true
}
This method doesn't work. However - if I use remove() and add() instead of replace(), then it works fine:
OpeningFragment.kt
val opening = fragmentManager?.findFragmentByTag("Opening")
transaction?.remove(opening!!)
transaction?.add(R.id.fragment_layout, playerDetailsFragment, "Add Player")
transaction?.commit()
gameString = "Add Player"
Any idea why this is?
For me replace works exactly as docs says. It removes all fragments in given container id and then adds new fragment with specified tag.
Your optional code removes only one fragment that you desire, not all of them, therefore it's not identical to replace function.
I have a pager adapter with 4 copies of the same fragment. Each of them has the same fragment container that has to be filled with another fragment when a specific view is clicked in its fragment. I do that with
getFragmentManager().beginTransaction().replace(R.id.gear_change_container,fragment).commit();
but that always put the fragment in the container of the fragment in the first tab.
How can I refer specifically to the container within the tab the click is made?
Here is an example how I would replace a fragment inside a fragment using Kotlin extension.
fun Fragment.replaceFragment(fragment: Fragment?, frameId: Int, addToBackStack: Boolean = true) {
fragment?.let {
childFragmentManager.beginTransaction()
.apply {
if (addToBackStack) addToBackStack(null)
}
.replace(frameId, fragment)
.commit()
}
}
Hope this will help.
I am using 3 different fragments. First in a fragment (fragment 1), I immediately call a child fragment (child) in onCreateView. Then in this child fragment, when I click on a button I call another different fragment (fragment 2). I add these 2 fragments (child and fragment 2) to BackStack.
But here is my issue, when I return from fragment 2, fragment 1 is called with savedInstance = null and I loose the state I had from the child fragment. My wish is to keep the state I had from the child fragment when I call fragment 2. How can I do that please ?
Here are my 2 fragments calls :
\\ Fragment 1 to Child
override fun onCreateView(savedInstanceState: Bundle?) {
super.onCreateView(savedInstanceState)
showChild(screen = Screen.WORKSPACE_HOME.create(), container = R.id.Frame_layout, transition = Transition.RIGHT, addToBackStack = true, isScreen = true)}
\\ Child to Fragment 2
override fun onItemClick(v: View, position: Int) {
val photo = items[position]
toolbarActivity?.show(screen = Screen.IMAGE_DETAILS_WORKSPACE.create().withArguments(ITEM to photo), container = R.id.container, transition = Transition.RIGHT, addToBackStack = true)
}