All the Fragment inherits from BaseFragment.
And I'd like to give back press event respectively to each fragment. But BaseFragment has default back press event like this.
override fun onResume() {
super.onResume()
logd("onResume() BaseFragment")
requireActivity().onBackPressedDispatcher.addCallback(this, backPressCallback)
}
And also the child fragments have
override fun onResume() {
super.onResume()
logd("onResume() ChildFragment")
requireActivity().onBackPressedDispatcher.addCallback(this, backPressCallback)
}
It will print,
onResume() BaseFragment
onResume() ChildFragment
So, the ChildFragment override and when I press back button, ChildFragment's backPressCallback is called.
However, when I go out and come back, the order is different.
onResume() ChildFragment
onResume() BaseFragment
So, the user sees ChildFragment but BaseFragment's backPressCallback is called. And it behaves different from what I expected. (ex. I want popBackStack but close the app)
How can I solve this problem? Or is there any method that called after the parent fragment is called?
According to this article, BaseFragment must be called before ChildFragment. But it doesn't seem so.
activity?.onBackPressedDispatcher?.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
try {
if (!isChildFragmentOpen)
{
val trans: FragmentTransaction =
parentFragmentManager.beginTransaction()
trans.remove(Fragment())
trans.commit()
}else{
parentFragmentManager.popBackStack()
}
} catch (e: Exception) {
e.message
}
}
})
add this to your fragment.
Related
My task is to dynamically go on back pressed when i want to go back from fragment.
I have bottom navigation which is in activity.
My Code:
MainActivity:
setCurrentFragment(topArticlesFragment)
binding.bottomNavigationView.setOnItemSelectedListener {
when (it.itemId) {
R.id.topArticles -> setCurrentFragment(topArticlesFragment)
R.id.allArticles -> setCurrentFragment(everythingArticlesFragment)
}
return#setOnItemSelectedListener true
}
private fun setCurrentFragment(fragment: Fragment) {
supportFragmentManager
.beginTransaction().apply {
replace(R.id.frame_layout, fragment)
this.addToBackStack(null)
commit()
}
}
TopArticlesFragment:
binding.recyclerViewTop.visibility = GONE
binding.searchView.visibility = GONE
parentFragmentManager
.beginTransaction()
.replace(R.id.topArticlesFragment, fragment)
.addToBackStack(null)
.commit()
}
DetailsFragment:
binding.toolbar.setNavigationOnClickListener {
activity?.onBackPressed()
}
Pictures of what is happening
I also tried with nav_graph but i put bottom navigation in MainFragment and when i choose another fragment bottom navigation disappeared.
Edit:
EDIT:
I also have onResume and onStop in TopArticleFragment and Everything article fragment to show and hide topMenu and bottom navigation. But when used binding.recyclerView.isVisible = true nothing happens.
override fun onResume() {
super.onResume()
(activity as AppCompatActivity?)!!.supportActionBar!!.show()
(activity as AppCompatActivity?)!!.findViewById<BottomNavigationView>(R.id.bottomNavigationView).isVisible =
true
}
override fun onStop() {
super.onStop()
(activity as AppCompatActivity?)!!.supportActionBar!!.hide()
}
I have following logic in my Activity :
protected abstract val fragment: BaseSearchFragment<T>
override fun onCreate(savedInstanceState: Bundle?) {
...
replaceFragmentInActivity(fragment, R.id.fragment_container)
}
Now when I rotate the device fragment Callbacks get called twice such as onCreate in the fragment, since I replaced the fragment in Activity.
I want to use onSaveInstanceState in the fragment, but since it is called twice the logic will not work as expected since for the 2nd fragment creation, savedInstanceState will be null.
If I use following in the Activity :
if(savedInstanceState == null) {
addFragmentToActivity(fragment, R.id.fragment_container)
}
I will face with another problem since I have following in the activity :
searchView.setOnQueryTextListener(object : OnQueryTextListener {
override fun onQueryTextChange(query: String): Boolean {
fragment.search(query)
return true
}
}
And here is fragment search method :
fun search(query: String) {
if (searchViewModel.showQuery(query)) {
binding.recyclerView.scrollToPosition(0)
tmdbAdapter.submitList(null)
}
}
Here when I am rotating the device I receive following exception :
java.lang.IllegalStateException: Can't access ViewModels from detached fragment
So, is there any solution that when I rotate the device fragment Callbacks get called just once?
You can check the source code here : https://github.com/alirezaeiii/TMDb-Paging
I have two Fragments A and B with RecyclerView list.
When fragment A is active I call:
fragmentTransaction.hide(Frag_A);
and I add fragment B with command:
fragmentTransaction.add(R.id.fragment_container, Frag_B);
When fragment B is active and I do some changes I need to silently call:
mAdapter.notifyDataSetChanged();
on Fragment A.
So it is possible to call this, when fragment is hidden?
Now I am using
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
}
But this method is triggered when I am going back from fragment B to A.
I want to refresh fragment A when fragment B is active. (User see fragment B list).
How to do that?
From my understanding, you want to refresh data in FragmentA while FragmentB is active.
First, you have create an Interface
interface FragmentCallback {
fun onResumeFragment()
}
Next, before you want to go to FragmentA, you are using add method. So, you have to add a tagfor FragmentA.
private fun loadFragmentA(someId: Int) {
val bun = Bundle()
bun.apply {
putInt(Constant.SOME_ID, someId)
}
val fragmentA = FragmentA()
fragmentA.arguments = bun
val fragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction =
fragmentManager.beginTransaction()
fragmentTransaction.add(
R.id.fragment_container,
fragmentA,
"TagFragmentA"
)
fragmentTransaction.commit()
}
At the MainActivity, you need to implement the interface that we created. inside the onResumeFragment method, we have to find that FragmentA by using tag. So, we can use the method inside that fragment.
class MainActivity : AppCompatActivity(), FragmentCallback {
override fun onResumeFragment() {
val fragmentA =
supportFragmentManager.findFragmentByTag("TagFragmentA") as FragmentA
fragmentA.refreshData()
}
}
Inside FragmentA, create refreshData method and make it public. So inside this method, you can notify the data changed.
fun refreshData(){
viewModel.getListSomething()
}
Inside FragmentB, you need to override the onAttach method to make the object for FragmentCallback.
private var fragmentCallback: FragmentCallback? = null
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is FragmentCallback) {
fragmentCallback = context
} else {
throw RuntimeException(
context.javaClass.simpleName
.toString() + " must implement FragmentCallback"
)
}
}
Finally create a method to trigger when there is data changes.
private fun timeToUpdateDataAtFragmentA(){
fragmentCallback?.onResumeFragment()
}
When you from FragmentB, back press to FragmentA, the data will update at the FragmentA.
Android Studio 3.4
I have a simple stock forecast app that uses single activity called ForecastActivity and 3 fragments called LoadingFragment, ForecastFragment, and RetryFragment
The RetryFragment will be added first using the following code in the
`ForecastActivity`:
private fun startRetryFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, RetryFragment(), "RetryFragment")
fragmentTransaction.commit()
}
}
I have a button in the RetryFragment that will start the ForecastActivity again.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnRetry.setOnClickListener {
startActivity(Intent(activity, ForecastActivity::class.java))
}
}
Then the LoadingFragment will start using this code in the ForecastActvity
private fun startLoadingFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, LoadingFragment(), "LoadingFragment")
fragmentTransaction.commit()
}
}
Then after loading has completed the ForecatActivity will start the ForecastFragment
private fun startForecastFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, RetryFragment(), "ForecastFragment")
fragmentTransaction.commit()
}
In my ForecastActivity I am trying to remove the fragments from the backstack, basically, from here I just want to finish the app. However, as I didn't add any to the backstack I don't expect the count to greater than zero. However, I was wondering how can I end the app and prevent the RetryFragment displaying again?
override fun onBackPressed() {
fragmentManager?.let {
if(it.backStackEntryCount > 0) {
it.popBackStackImmediate()
}
else {
super.onBackPressed()
}
}
}
However, when I click the back from when I am on the ForecastFragment I expect the application to finish. However, the RetryFragment is displayed and then I have to click the back button again to end the app.
Is there something wrong doing this in the RetryFragment
Basically, I just want to go back to the ForecastActivity from the RetryFragment to start again.
startActivity(Intent(activity, ForecastActivity::class.java))
Many thanks for any suggestions.
I think your issue is that you're re-launching the ForecastActivity again from the RetryFragment. If you restructure your code so that the RetryFragment can trigger a retry function in ForecastActivity, you could just replace the RetryFragment with the next appropriate fragment instead of calling startActivity() to create a new ForecastActivity.
I have an activity and several fragments that it replaces inside a frame layout. Each fragment contains a layout inflated from XML with a number of custom views. Inside those views, I want to subscribe to lifecycle events in these views using LifecycleObserver. My view in Kotlin:
class MyView(context: Context) : View(context, null, 0): LifecycleObserver {
init {
(getContext() as LifecycleOwner).lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
// code
}
#OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
// code
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
lifecycle.removeObserver(this)
}
}
The problem is, when one fragment goes away and is replaced by another, the views in the first fragment don't receive an onPause event. Nor do they get onResume when I return to it from the second fragment. The views receive onPause only when the whole activity is paused, but they are unaware of lifecycle changes in the fragments. I tracked this down to layout inflater, that is used to inflate fragment's xml layout, it passes the activity as context parameter to views. This is how layout inflater is instantiated in support library's Fragment class:
/** #deprecated */
#Deprecated
#NonNull
#RestrictTo({Scope.LIBRARY_GROUP})
public LayoutInflater getLayoutInflater(#Nullable Bundle savedFragmentState) {
if (this.mHost == null) {
throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the Fragment is attached to the FragmentManager.");
} else {
LayoutInflater result = this.mHost.onGetLayoutInflater();
this.getChildFragmentManager();
LayoutInflaterCompat.setFactory2(result, this.mChildFragmentManager.getLayoutInflaterFactory());
return result;
}
}
The mHost is a FragmentActivity that contains this fragment. Hence, LayoutInflater that's passed into fragment's onCreateView() contains a reference to the FragmentActivity as a context. So the views effectively observe Activity lifecycle.
How do I make my custom views observe lifecycle events of their containing fragment?
If you're only using onPause() and onResume(), overriding onDetachedFromWindow() and onAttachedToWindow() in your View class should be enough:
override fun onAttachedToWindow() {
//onResume code
}
override fun onDetachedFromWindow() {
//onPause code
}
You could also make your own lifecycle methods:
fun onResume() {}
fun onPause() {}
From your Fragment, you can retain a global reference to your View:
//`by lazy` only initializes the variable when it's fist called,
//and then that instance is used for subsequent calls.
//Make sure you only reference `myView` after `onCreateView()` returns
private val myView by lazy { view.findViewById<MyView>(R.id.my_view) }
And then from your Fragment's onPause() and onResume(), call your View's corresponding methods:
override fun onPause() {
myView.onPause()
}
override fun onResume() {
myView.onResume()
}
EDIT:
For expandability, make your own min-SDK. Create a base Fragment class:
open class LifecycleFragment : Fragment() {
internal fun dispatchOnResume(parent: View) {
if (parent is CustomLifecycleObserver) parent.onResume()
if (parent is ViewGroup) {
for (i in 0 until parent.childCount) {
dispatchOnResume(parent.getChildAt(i))
}
}
}
internal fun dispatchOnPause(parent: View) {
if (parent is CustomLifecycleObserver) parent.onPause()
if (parent is ViewGroup) {
for (i in 0 until parent.childCount) {
dispatchOnResume(parent.getChildAt(i))
}
}
}
override fun onResume() {
dispatchOnResume(view)
}
override fun onPause() {
dispatchOnPause(view)
}
}
(CustomLifecycleListener would be an interface for your Views to implement, containing onResume() and onPause() methods.)
Then just extend that class in your other Fragments:
class SomeFragment : LifecycleFragment() {}