why do we pass viewLifecycleOwner and Lifecycle.State.RESUMED to addMenuProvider?
`private fun addOptionMenu() {
val activity = requireActivity()
activity.addMenuProvider(object : MenuProvider{
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.edit_menu,menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when(menuItem.itemId){
R.id.action_next -> true
R.id.action_settings -> true
else -> false
}
}
})
}`
what does they do?
I tried to remove them and didn't notice any change
There 3 addMenuProvider i.e.
addMenuProvider(MenuProvider)
The menu is added irrespective of the lifecycle & its current state.
addMenuProvider(MenuProvider provider, LifecycleOwner)
The menu is added instantly but is removed when the lifecycle hits ON_DESTROY.
addMenuProvider(MenuProvider, LifecycleOwner, Lifecycle.State)
The menu is added when the state is atleast the provided state, & is removed when the lifecycle hits ON_DESTROY.
Why you ask?
You don't want your app to crash when you click a MenuItem & access the view where the Fragment's View might have been destroyed.
Example: TabLayout + ViewPager(2) & a lot of fragments with default offscreenPageLimit.
Hence the lifecycle simply ensures crash safety when you access the MenuItem.
Related
I have a single activity app that uses navigation graph and a navigation drawer to go to some of the fragments. Pressing back from each of the fragments usually brings me back to the main fragment, UNLESS I turn the screen off and back on or I put the app in the background. When I resume the app, the up button widget turns back into a hamburger menu, but navigation doesn't happen. Pressing the android back button doesn't navigate either, as if the app forgets where to navigate to.
val navController = (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
setSupportActionBar(layoutAppBarMain.layoutToolbarMain)
NavigationUI.setupActionBarWithNavController(this#MainActivity, navController, mainDrawerLayout)
appBarConfiguration = AppBarConfiguration(navController.graph, mainDrawerLayout)
NavigationUI.setupWithNavController(mainActivityNavView, navController)
supportActionBar?.setDisplayShowTitleEnabled(false)
navController.addOnDestinationChangedListener { _: NavController, nd: NavDestination, _: Bundle? ->
when (nd.id) {
R.id.playFragment -> mainDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
R.id.navRulesFragment, R.id.navImproveFragment, R.id.navAboutFragment, R.id.navDonateFragment -> mainDrawerLayout.setDrawerLockMode(
DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
else -> {
binding.layoutAppBarMain.layoutToolbarMain.navigationIcon = null
mainDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
}
}
Then overriding the onSupportNavigateUp():
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp()
}
If you don't have below code just add and try again:
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null) || super.onSupportNavigateUp()
}
Edit: If that doesn't work then try this:
layoutAppBarMain.layoutToolbarMain.setNavigationOnClickListener { onBackPressed() }
I found out what was causing it. I was using postponeEnterTransition() to check if the database was empty. If it is, then it should load the first fragment, else the second fragment. The problem was that I was using startPostponedEnterTransition() after the navigation had already left the first fragment, which was causing the navController to misbehave.
To solve this, I am now starting the enter transition in the first fragment, then hiding it, before navigating to the second fragment. I am also using a splash screen which is being turned off after the navigation is complete.
In MainActivity:
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { mainVm.keepSplashScreen.value }
super.onCreate(savedInstanceState)
In onCreateView of the first fragment:
postponeEnterTransition() // Wait for data to load before displaying fragment
mainVm.matchState.observe(viewLifecycleOwner, EventObserver { matchState ->
when (matchState) {
RULES_IDLE -> mainVm.transitionToFragment(this, 0)
// Here, if database is not empty,
// start transition right away and hide the view only then navigate
// If the transition happens after the navigation has started,
// the navController will have issues returning from the navDrawer fragments.
GAME_IN_PROGRESS -> {
startPostponedEnterTransition()
view?.visibility = GONE
navigate(RulesFragmentDirections.gameFrag()
}
else -> Timber.e("No implementation for state $matchState at this point")
}
})
Within the MainViewModel:
private val _keepSplashScreen = MutableStateFlow(true)
val keepSplashScreen = _keepSplashScreen.asStateFlow()
fun transitionToFragment(fragment: Fragment, ms: Long) = viewModelScope.launch {
fragment.startPostponedEnterTransition()
delay(ms) // Add a delay for the content to fully load before turning off the splash screen
_keepSplashScreen.value = false
}
Aside from this, the navController implementation works correctly as per the post above.
Since the old Activity.onBackPressed() becomes deprecated starting Android 33, what is the better way to call it programmatically?
Example:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Handle default back arrow click
android.R.id.home -> {
onBackPressed()
}
...
We could create and add OnBackPressedCallback to the onBackPressedDispatcher like this.
onBackPressedDispatcher.addCallback(
this, // Lifecycle owner
backPressedCallback
)
private val backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (viewPager.currentItem != 0)
viewPager.setCurrentItem(0, true)
else
finish()
}
}
Then replace the old onBackPressed with
// Handle default back arrow click
android.R.id.home -> {
backPressedCallback.handleOnBackPressed()
}
But I saw this public method in onBackPressedDispatcher and wondering if I could use it instead.
onBackPressedDispatcher.onBackPressed()
Does this method iterates on each OnBackPressedCallback that has been added in the onBackPressedDispatcher?
So basically onBackPressedDispatcher.onBackPressed() is the same as Activity.onBackPressed() and you can use it in the same manner if you don't care about precise navigation. How do I know that? - well, you can see the source code of the ComponentActivity(basically a parent of a regular Activity you are using), and its onBackPressed() looks like this:
#Override
#MainThread
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}
Regarding it calling over the callbacks queue - you are correct also, but it is not iterating over it - but just calling the most recent one - one at a time(per back press or per onBackPressed() call trigger), the documentation states:
public void onBackPressed()
Trigger a call to the currently added callbacks in reverse order in which they were added. Only if the most recently added callback is not enabled will any previously added callback be called.
If hasEnabledCallbacks is false when this method is called, the fallback Runnable set by the constructor will be triggered.
So your strategy here might be like this - if you need some specific stuff to be executed before the back navigation - you add it to the handleOnBackPressed of the callback. If no special behavior needed - you can just call mOnBackPressedDispatcher.onBackPressed() - it will still call the most recently added(if it is there of course) callback method, but if it is empty - the back will work just fine.
You need to keep in mind, though, that there are two overrides of addCallback methods:
addCallback(#NonNull OnBackPressedCallback onBackPressedCallback)
and
public void addCallback(
#NonNull LifecycleOwner owner,
#NonNull OnBackPressedCallback onBackPressedCallback
)
In the former - you have to handle the callback queue by yourself calling remove on callback when you need it not to be executed anymore. In the latter - LifecycleOwner state change has to handle all the needed stuff for you.
More info here and here.
I am showing the Cast button as an Options menu item that is inflated from an activity, but I noticed that when the activity has a child fragment and the child fragment does not have an options menu item by itself, the chrome cast introduction overlay works correctly. However when the fragment has its own options menu, the Cast introduction overlay does not work correctly, it either shows in the top left corner or shows up in the correct position but does not highlight the cast icon.
Here is the code to initialize the Overlay
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
loadCastButton(menu)
return super.onCreateOptionsMenu(menu)
}
private fun loadCastButton(menu: Menu?) {
menuInflater.inflate(R.menu.menu_cast, menu)
CastButtonFactory.setUpMediaRouteButton(applicationContext, menu, R.id.cast_menu_item)
val mediaRoutebutton = menu?.findItem(R.id.cast_menu_item)?.actionView as? MediaRouteButton
mediaRoutebutton?.dialogFactory = CastDialogFactory()
handleCastTutorial(menu)
}
private fun handleCastTutorial(menu: Menu?) {
val castButton = menu?.findItem(R.id.cast_menu_item)
if (castButton == null) {
return
}
castViewModel.isCastingAvailable.observe(this) {
if (it == true && castButton.isVisible) {
//Show cast tutorial
castViewModel.setCastTutorialShown(true)
IntroductoryOverlay.Builder(this, castButton)
.setTitleText(R.string.cast_tutorial_title)
.setSingleTime()
.build()
.show()
}
}
}
When you are showing Cast buttons in fragments and activities, menus are inflated everywhere, with Cast buttons initialized in one of the fragments or activities and then immediately hidden again. My recommended solution is delaying the cast tutorial with a minor amount of delay, and then checking for visibility and window attach status again:
if (!castViewModel.getCastTutorialShown()) {
binding.root.postDelayed(200L) {
// Check if it is still visible.
if (castButton.isVisible && castButton.actionView.isAttachedToWindow && !castViewModel.getCastTutorialShown()) {
castViewModel.setCastTutorialShown(true)
IntroductoryOverlay.Builder(this, castButton)
.setTitleText(R.string.cast_tutorial_title)
.setSingleTime()
.build()
.show()
}
}
}
I have a fragment A which sends a search query to the network, and if the result is positive uses Android navigation component to navigate to fragment B, and its done using observers.
After navigation to fragment B, i click on "<-" arrow on the top of the screen, but instead of navigating back to fragment A it reloads fragment B again. And if using the native "back" button on the device, the app crashes with "illegalArgumentException navigation destination unknown" error.
I check the internet for clues on this issue, but all i learned is that this happens because i am using .observe in onViewCreated() and when i go back, it gets called again, and because livedata has something in it already, it just navigates me back to B.
I have tried observing in onActivityCreated(), and using getViewLifeCycleOwner, but no success... the only thing that helped is checking if livedata has observers and returning if true, before using .observe, but it seems incorrect.
This is the viewModel:
private val getAssetResult = MutableLiveData<GeneralResponse<Asset>>()
private val updateAssetResult = MutableLiveData<GeneralResponse<Int>>()
private val deleteAssetResult = MutableLiveData<GeneralResponse<Int>>()
init {
state.value = ViewState(false)
Log.d(TAG, "State in init: $state")
}
fun getAssetResult(): LiveData<GeneralResponse<Asset>>{
return getAssetResult
}
fun findAsset(req: GetAssetRequest) {
scope.launch {
setProgressIndicator(true)
val result = repository.getAsset(req)
getAssetResult.postValue(result)
setProgressIndicator(false)
}
}
This is the fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(EditAssetViewModel::class.java)
setupViewModel()
initFields()
}
private fun setupViewModel() {
if (viewModel.getAssetResult().hasObservers()) // <- This is the part that prevents the app from crashing.
return
viewModel.getAssetResult().observe(this, Observer {
if (it == null) return#Observer
handleSearchResult(it)
})
if (viewModel.getState().hasObservers())
return
viewModel.getState().observe(this, Observer { handleState(it) })
}
private fun handleSearchResult(response: GeneralResponse<Asset>) {
if (response.singleValue == null) {
Toast.makeText(context!!, response.errorMessage, Toast.LENGTH_SHORT).show()
return
}
targetFragment?.let { it ->
val bundle = bundleOf("asset" to response.singleValue)
when(it) {
"UpdateLocation" ->
Navigation.findNavController(view!!).navigate(R.id.updateLocation, bundle)
"EditAsset" -> {
Navigation.findNavController(view!!).navigate(R.id.editAsset, bundle)
}
}
}
}
if i remove this part from the setupViewModel function:
if (viewModel.getAssetResult().hasObservers())
return
the app will either crash when clicked "back" using the device button or go back to fragment A, just to be navigated back to fragment B because of the .observe function.
Override the method onBackPressed() to handle the "<-" arrow
Seems like the LiveData that you use to signal to fragment A that it should navigate to fragment B is actually an event. An event happens only once and once it is consumed (navigation event is done), it is gone. Therefore, after navigating you need to send a message to the viewmodel that the navigation took place and that the corresponding data holder should be (e.g.) null again. In Fragment A you check that the new value is unequal to null, and only if this is the case, you issue the navigation event. This would prevent fragment A to immediatelly switch to B again in the back scenario.
If you want to learn more about ways to use live data for events, please refer to this article.
I have an activity with two fragments.
The second one is called when I click on something to the first.
What I want is this : if i click on "back" button, I want to go back to the first fragment (that is working), but I want to set the visibility to VISIBLE on an element (if the first fragment is called with back press only)
How do I do that ?
I tried something like this (in my main fragment), I've found the idea in another topic, but this is trigger always in my main activity :
override fun onResume() {
super.onResume()
view?.isFocusableInTouchMode = true
view?.requestFocus()
view?.setOnKeyListener { v, keyCode, event ->
if(event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK){
Log.i(TAG, "backpress pressed")
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
Temporary solution :
I've created a companion object with a value true or false and I change it everytime I need it, but it's temporary only.
Assuming your second Fragment replaces the first (i.e. using FragmentTransaction#replace), your first Fragment (we'll call them FragmentA and FragmentB) will be paused (i.e. onPause() will be called on FragmentA).
When you press the back button, the backstack will be popped, and FragmentA will be resumed (i.e. onResume() will be called).
What I would recommend, is to save a boolean flag in FragmentA, and set it to true when you show FragmentB. Then, in FragmentA#onResume, you can check if the flag is set to true, and set it back to false while handing the case that you wanted.
For example, something like:
private const val STATE_WAITING_FOR_FRAGMENT_B = "state_waiting_b"
class FragmentA : Fragment() {
private var isWaitingForFragmentB: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
isWaitingForFragmentB = savedInstanceState.getBoolean(STATE_WAITING_FOR_FRAGMENT_B)
}
}
override fun onResume() {
super.onResume()
if (isWaitingForFragmentB) {
isWaitingForFragmentB = false
// handle your view state here
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(
STATE_WAITING_FOR_FRAGMENT_B,
isWaitingForFragmentB
)
}
private fun showFragmentB() {
isWaitingForFragmentB = true
// do fragment transaction here
}
}
I'm not good at grammar.
First fragment do not call resume function when returning.
You must create callback with interface.
A good approach should be passing some flag, on the second fragment, by activity intent and to capture it on the first Fragment on onResume()
If you need extra info, just let me know