How to prevent crashes during quick tapping on bottomNavigationView android? - android

In my app I have main activity with container and bottomNavigationView with 5 items. At container I can show fragments with information. I can change fragment by clicking on buttons which are placed on bottomnavigation view. I noticed the problem:
when I click on items very quickly I receive or app crashes because my view is looking for view-elements or another problem is that my parent activity toolbar show wrong title/subtitle. For example I can click on item 1 and then quickly tap on item 2 and if I continue tapping toooo quickly I can see that toolbar show content which demand to wrong fragment. I thought that maybe checking fragment visibility will solve this problem but it didn't help me. I tried also to check if fragment is visible from onCreateView at fragment and it didn't help me too. Here is the code of bottomNavigationView listener:
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
val transaction = supportFragmentManager.beginTransaction()
val bundle = Bundle()
when (item.itemId) {
R.id.item_id_1-> {
supportActionBar!!.show()
[....]
transaction.replace(R.id.contentContainerT, jobList).addToBackStack(null).commit()
textSetter("Item_0", resources.getString(R.string.all_jobs) + " " + sp!!.getString("all_jobs", ""))
return#OnNavigationItemSelectedListener true
}
R.id.item_id_2-> {
bottom_navigation_t.menu.findItem(R.id.received_mess).title.chunked(1)
disableShowHideAnimation(supportActionBar!!)
supportActionBar!!.show()
[.....]
textSetter(resources.getString(R.string.title_activity_message_center), resources.getString(R.string.received))
transaction.replace(R.id.contentContainerT, messageList).addToBackStack(null).commit()
return#OnNavigationItemSelectedListener true
}
R.id.item_id_3-> {
disableShowHideAnimation(supportActionBar!!)
supportActionBar!!.hide()
transaction.replace(R.id.contentContainerT, PersonalPage()).addToBackStack(null).commit()
return#OnNavigationItemSelectedListener true
}
R.id.item_id_4-> {
disableShowHideAnimation(supportActionBar!!)
supportActionBar!!.show()
[....]
transaction.replace(R.id.contentContainerT, notepadScr).addToBackStack(null).commit()
return#OnNavigationItemSelectedListener true
}
R.id.item_id_5-> {
disableShowHideAnimation(supportActionBar!!)
supportActionBar!!.hide()
textSetter(resources.getString(R.string.more_bottom_nav), "")
transaction.replace(R.id.contentContainerT, MoreScreenK()).addToBackStack(null).commit()
return#OnNavigationItemSelectedListener true
}
}
false
}
maybe problem at the setter function, I don't know.

Related

MutableStateFlow is triggered after returning to the app

In my MainActivity I have BottomNavigation. My activity is connected with MainViewModel. When app starts I fetch data from firebase. Until the data is downloaded, app displays ProgressBar and BottomNavigation is hide (view.visibility = GONE). When data has been downloaded I hide ProgressBar and show BottomNavigation with the app's content. It works great.
In another part of the app user can open gallery and choose photo. The problem is when activity with photo to choose has been closed, MutableStateFlow is triggered and bottomNavigation displays again but it should be hide in that specific part(fragment) of the app.
Why my MutableStateFlow is triggered although I don't send to it anything when user come back from gallery activity?
MainActivity (onStart):
private val mainSharedViewModel by viewModel<MainSharedViewModel>()
override fun onStart() {
super.onStart()
lifecycle.addObserver(mainSharedViewModel)
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val navHostFragment: FragmentContainerView = findViewById(R.id.bottomNavHostFragment)
bottomNavController = navHostFragment.findNavController()
bottomNavigationView.apply {
visibility = View.GONE
setupWithNavController(navHostFragment.findNavController())
}
//the fragment from wchich I open GalleryActivity is hide (else ->)
navHostFragment.findNavController().addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.mainFragment,
R.id.profileFragment,
R.id.homeFragment -> bottomNavigationView.visibility = View.VISIBLE
else -> bottomNavigationView.visibility = View.GONE
}
}
mainSharedViewModel.viewModelScope.launch {
mainSharedViewModel.state.userDataLoadingState.collect {
if (it == UserDataLoading.LOADED) {
bottomNavigationView.visibility = View.VISIBLE
} else {
bottomNavigationView.visibility = View.GONE
}
}
}
}
ViewModel:
class MainSharedViewState {
val userDataLoadingState = MutableStateFlow(UserDataLoading.LOADING) }
enum class UserDataLoading {
LOADING, UNKNOWN_ERROR, NO_CONNECTION_ERROR, LOADED }
When you come back from the gallery, the stateflow value is still set as Loaded, as the Viewmodel has not been cleared (and the activity was set to Stopped, not destroyed. It is still in the back stack.) This is why the bottomNavigationView is visible when you come back.
Although your architecture/solution is not how I would have done it, in your circumstances I guess you could change the value of the MutableStateFlow when the activity's onStop is called. Either that or use a MutableSharedFlow instead with a replayCount of 0 so that there is no value collected (although then, the bottomNavigationView will still be set as Visible if it is visible by default in XML.)
SOLVED:
I've created
val userDataLoadingState = MutableSharedFlow<UserDataLoading>(replay = 0)
and when my ViewModel is created I set
state.userDataLoadingState.emit(UserDataLoading.LOADING)
and I collect data in Activity
lifecycleScope.launch {
mainSharedViewModel.state.userDataLoadingState.collect {
if (it == UserDataLoading.LOADED) {
bottomNavigationView.visibility = View.VISIBLE
} else {
bottomNavigationView.visibility = View.GONE
}
}
}
Now it works great. I don't know why it didn't work before.

bottom navigation view fragment re-created when it is already created

In my application i am using Jetpack Navigation with BottomNavigationView. I have like 4 fragments:Home,Search,Notifications,Profile and when i am in Home fragment, i click again home item in bottom navigation view, it re-creates the fragment. I searched, but mainly answers were for those who did not use jetpack navigation.
(by the way, i only want fragment not being re-created when i am on that fragment already, if i am not in that fragment, it is okay to be re-created)
Below is my setup:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainerView_mainActivity) as NavHostFragment
navController = navHostFragment.navController
binding.bottomNavView.setupWithNavController(navController)
I read the source code of Google. I saw that, It always creates new fragment.
You have a bottom navigation like the app that I'm building. :))
For me, I didn't use menu item for bottom navigation view. I added a custom view for it.
(I have MainActivity, MainViewModel for managing the action bar, bottom navigation view .)
And then in custom view, when the customer click on item, I will check the page that they want to open is the same with the current page or not. If they are the same, I will not open it. Like this:
fun openHomePage() {
if (pageID.value != R.id.nav_home) {
pageID.postValue(R.id.nav_home)
}
}
pageID stores the id of current page:
var pageID = MutableLiveData<Int>()
private set
In MainActivity:
mainViewModel.pageID.observe(this, Observer {
val currentPageId = findNavController(R.id.nav_host_fragment).currentDestination?.id
if (it != 0 && it != currentPageId) {
drawerLayout.close()
navigatePageWithId(it)
}
})
This is a bug that has been around for a while and Google has not provided the official way to deal with it. More info is that because there is only one stack that swap in and out the fragment, you can read more from the SO's post
Android JetPack navigation with multiple stack
But you're using kotlin, you can refer this Github's repo where they provided a workaround for this situation
I ended up using code below.(Considering there is not best solution, it works for me as i want)
currentFragmentIndex is the integer value declared in above scope which shows the fragment we are currently in.
binding.bottomNavView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.homeFragment -> {
if (currentFragmentIndex == 0) {
false
} else {
currentFragmentIndex = 0
navController.navigate(R.id.homeFragment)
true
}
}
R.id.searchFragment -> {
if (currentFragmentIndex == 1) {
false
} else {
currentFragmentIndex = 1
navController.navigate(R.id.searchFragment)
true
}
}
R.id.notificationsFragment -> {
if (currentFragmentIndex == 2) {
false
} else {
currentFragmentIndex = 2
navController.navigate(R.id.notificationsFragment)
true
}
}
R.id.myProfileFragment -> {
if (currentFragmentIndex == 3) {
false
} else {
currentFragmentIndex = 3
navController.navigate(R.id.myProfileFragment)
true
}
}
else -> false
}
}
This is the right way to prevent fragments from recreation while using bottom navigation via Jetpack Navigation
binding.bottomNavView.setOnNavigationItemReselectedListener {
// Do nothing to ignore the reselection
}

Elements quit responding to visibility changes after switching fragments

I am creating an app that holds a recyclerview in a fragment and the main activity uses a bottom navigation bar to switch fragments. In my main fragment, when a recyclerview item is long clicked, an appbar appears with some buttons and checkboxes. When I press the back button, I would like to hide their visibility to return the fragment to its default state.
My issue is that after first starting the app, the items appear from the long click and then hide when the back button is pressed as it should. If I switch to another fragment, and then switch back, the back button still responds but the elements no longer respond to visibility changes.
This is my onBackPressed() function in my MainActivity.kt:
#SuppressLint("RestrictedApi")
override fun onBackPressed() {
btnDelete.visibility = View.GONE
btnDelete.isVisible = false
radioButton.visibility = View.GONE
radioButton.isVisible = false
constrain.visibility = View.GONE
constrain.isVisible = false
radioButton.isChecked = false
radioButton.isSelected = false
fabButton.visibility = View.VISIBLE
fabButton.isVisible = true
(recyclerView_main.adapter as NoteAdapter).hideItems()
}
This is my method of switching fragments in my MainActivity.kt:
if(savedInstanceState == null){
this.supportFragmentManager.beginTransaction()
.replace(R.id.frag_container, selectedFragment).commit()
}
navView.setOnNavigationItemSelectedListener{
for (i in 0 until supportFragmentManager.backStackEntryCount){
this.supportFragmentManager.popBackStack()
}
when(it.itemId){
R.id.nav_home ->{
this.supportFragmentManager.beginTransaction().remove(selectedFragment).commit()
selectedFragment = HomeFragment()
}
R.id.nav_fav ->{
this.supportFragmentManager.beginTransaction().remove(selectedFragment).commit()
selectedFragment = FavoriteFragment()
}
R.id.nav_trash ->{
this.supportFragmentManager.beginTransaction().remove(selectedFragment).commit()
selectedFragment = TrashFragment()
}
}
this.supportFragmentManager.beginTransaction()
.replace(R.id.frag_container, selectedFragment).commit()
true
}

Multiple Fragments in each page of a BottomNavigationView

I use a BottomNavigationView with four pages. If I want to add 4 fragments to that it is Ok but when I want to replace new fragment with old fragment in one of the BottomNavigationView page and restore it when item click, the first fragment open again. How can I restore the last fragment? Should I use different FrameLayout in the first BottomNavigationView?
Try this:
bottomNavView.setOnNavigationItemSelectedListener(object : BottomNavigationView.OnNavigationItemSelectedListener {
override fun onNavigationItemSelected(item: MenuItem): Boolean {
clearAllBackStack()
when (item.itemId) {
R.id.itemHome -> {
// load fragment here
}
R.id.itemMyAccount -> {
// load fragment here
}
R.id.itemSettings -> {
// load fragment here
}
R.id.itemNotfications -> {
// load fragment here
}
}
return true
}
})
add this method:
fun clearAllBackStack() {
for (i in 0 until supportFragmentManager.backStackEntryCount) {
supportFragmentManager.popBackStack()
}
}
Note: also add fragment to backstack whenever you load fragment, add this when you load fragment: fragmentTransaction.addToBackStack(null)

BottomNavigationView lags on fragment transaction

The problem
I'm using the BottomNavigationView from the Android Design Support Library on one of my Activities, alongside with Fragments for each navigation item.
Each time I select an item on the bar, I do a fragment transaction, like the snippet below (some parts of the code was removed for brevity):
private var fragmentToSet: Fragment? = null
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
fragmentToSet = when (item.itemId) {
// Choose fragment based on selection
// ...
}
// ...
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragmentToSet)
.commit()
}
The problem is... The bottom bar animation gets super laggy, and only finishes after the fragment is fully loaded and displayed on the screen.
This issue is not exactly new since it can also happen while using the Navigation Menu, but at least it's possible to solve it by using the DrawerLayout.DrawerListener and do the actual Fragment transaction only after the drawer is closed.
What I've tried so far
I tried to "cache" the fragments, holding their reference to avoid recreating the objects every time (e.g. MyFragment.newInstance()), but that didn't work.
I also tried to use handlers, which kinda solved the problem, but it might lead me to an exception in some cases. Something like the snippet below:
handler.postDelayed({changeFragment(fragmentToSet!!)}, 200)
Is there a way to solve this issue without using handlers (or other async calls), on a similar fashion to this solution while using the Navigation Menu?
I handled this situation by hiding and showing fragments using fragment manager. I wrote a sample code to deal with it as below.
class MainActivity : BaseActivity() {
private val homeFragment = HomeFragment.newInstance()
private val categoryFragment = CategoryFragment.newInstance()
private val searchFragment = SearchFragment.newInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.menu.findItem(R.id.navigation_home).isChecked = true
supportFragmentManager.beginTransaction()
.add(R.id.containerFrameLayout, homeFragment)
.add(R.id.containerFrameLayout, categoryFragment)
.add(R.id.containerFrameLayout, searchFragment)
.commit()
setTabStateFragment(TabState.HOME).commit()
}
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0 || !homeFragment.isHidden) {
super.onBackPressed()
} else {
setTabStateFragment(TabState.HOME).commit()
navigation.menu.findItem(R.id.navigation_home).isChecked = true
}
}
private fun setTabStateFragment(state: TabState): FragmentTransaction {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
val transaction = supportFragmentManager.beginTransaction()
transaction.setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
when (state) {
TabState.HOME -> {
transaction.show(homeFragment)
transaction.hide(categoryFragment)
transaction.hide(searchFragment)
}
TabState.CATEGORY -> {
transaction.hide(homeFragment)
transaction.show(categoryFragment)
transaction.hide(searchFragment)
}
TabState.SEARCH -> {
transaction.hide(homeFragment)
transaction.hide(categoryFragment)
transaction.show(searchFragment)
}
}
return transaction
}
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
setTabStateFragment(TabState.HOME).commit()
return#OnNavigationItemSelectedListener true
}
R.id.navigation_category -> {
setTabStateFragment(TabState.CATEGORY).commit()
return#OnNavigationItemSelectedListener true
}
R.id.navigation_search -> {
setTabStateFragment(TabState.SEARCH).commit()
return#OnNavigationItemSelectedListener true
}
}
false
}
internal enum class TabState {
HOME,
CATEGORY,
SEARCH,
}
}

Categories

Resources