Android repeatOnLifecycle in fragment inside ViewPager2 - android

If I add the following code snippet to a "normal" fragment it gets started and cancelled as expected when navigating to and from the fragment, but if I add this to fragment inside a view pager 2 it is not cancelled even though the fragmens onPause method is invoked. Is this by design or am I missing something?
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
try {
while (isActive) {
println("Fragment alive....")
delay(1000)
}
} catch (ex: CancellationException) {
println("Cancelled fragment...")
throw ex
}
}
}

I am no sure if I got your question right, but I can already tell that you're relaying on the wrong lifecycle event if you wish that your code gets executed when the Fragment is visible, for that you need to use repeatOnLifecycle(Lifecycle.State.RESUMED). Using this your code will start executing as soon as the Fragment is visible and gets cancelled when it gets paused.
Using repeatOnLifecycle(Lifecycle.State.STARTED) your code will start executing when the Fragment is started (ready to get displayed) and gets cancelled when it gets stopped.

Related

Fragment getting initialised before Android 12 Splash screen ends

I have a FragmentContainerView in my MainActivity which uses a nav graph.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav" />
I want my fragments to wait till I get some data from an API before I hide my splash screen. I am using android 12 Splash Screen. This is how I am trying to accomplishing it(from documentation):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
setContentView(R.layout.activity_main)
//Set up an OnPreDrawListener to the root view.
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
// Check if the initial data is ready.
return if (mainActivityViewModel.isDataReady()) {
// The content is ready; start drawing.
Timber.tag("Splash").d("data ready")
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
Timber.tag("Splash").d("data not ready")
// The content is not ready; suspend.
false
}
}
}
)
The Splash screen is going away only after I get the data. But the issue is that my Fragments callbacks like onViewCreated gets invoked even before my data is ready. This is causing issues because I rely on data that I fetch during splash screen to do some tasks.
How can I make sure, my fragments won't get initialised before my Splash screen goes away??
The Splash screen is going away only after I get the data. But the issue is that my Fragments callbacks like onViewCreated gets invoked even before my data is ready.
Activity and fragment lifecycle callbacks are only triggered by the system on the appropriate times. They can't be triggered by developers; there is no control on that. So, you can't stop onCreateView, onViewCreated,.. from taking place. i.e. you can't stop activity/fragment from initialization; otherwise you could have ANRs.
More in detail
Initializations of activities/fragments shouldn't be seized until some background task finishes; this literally will cause ANRs.
While you're seizing the drawing of the activity's root layout using addOnPreDrawListener; this doesn't mean that the activity's initialization (i.e. onCreate()) get seized, because both are working asynchronously, and the onCreate(), onStart(), & onResume() will get return normally.
The same is true for fragment's callbacks where the fragment's initialization (i.e. onCreateView(), onViewCreated...) is coupled with the activity's onCreate().
Now, as the initialization of the start destination fragment relies on the API data; then this particular fragment shouldn't be the start destination.
So, to fix this you need to create a some splash screen fragment as the start destination that doesn't rely on the API data.
Depending on the API incoming data, you can decide a navigation to the original fragment through the navController:
return if (mainActivityViewModel.isDataReady()) {
// Do the transaction to the original fragment through the navController
// The content is ready; start drawing.
Timber.tag("Splash").d("data ready")
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
// Not transaction is needed, keep the splash screen fragment
Timber.tag("Splash").d("data not ready")
// The content is not ready; suspend.
false
}
}
}

PagingDataAdapter.refresh() not working after fragment navigation

I am using PagingDataAdapter in one fragment to show user activity.
in fragment class level,
private var activityAdapter: ActivityFeedAdapter? = null
in onCreate() I am initializing before use as,
activityAdapter = initAdapter()
also in onCreate(),
this.lifecycleScope.launchWhenResumed {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.getActivityFeed().observe(viewLifecycleOwner) {
it?.let {
activityAdapter?.submitData(lifecycle, it)
}
}
}
}
and after onStart(), I am setting a click Listener on a view to refresh pagingdata from the UI as,
binding?.refresh?.setOnClickListener {
activityAdapter?.refresh()
}
Everything works fine when I use it for the first load. But after I navigate to some fragment and get back to the same screen, clicking on refresh only handles click event but does not refresh the adapter.
BTW, I have initialized the adapter in onCreate() because I need the adapter to maintain loaded data across screen transitions. Anyone help me...
I got the bug... :))
In onCreate() I was setting observer with lifecycleOwner as viewLifecycleOwner.
But viewLifecycleOwner is only active from onCreateView() till onDestroyView(). So after navigation to a different fragment and getting back from there, the new observer was not set. The old observer is canceled due to lifecycleOwner is destroyed. So I could refresh more data in PagingDataAdapter.
When setting the observer please rethink which lifecycleOwner is to be used. Hope this might help someone. :)

FragmentStateAdapter crashes, throwing java.lang.IllegalStateException

In my app, I have a PlaylistMenuFragment with a button that replaces itself in the ViewPager with PlaylistContentFragment, and vice-versa, through the following methods:
fun goToPlaylistContent() {
pagerAdapter.replaceLastFragmentWith(PlaylistContentFragment())
}
fun goToPlaylistMenu() {
pagerAdapter.replaceLastFragmentWith(PlaylistMenuFragment())
}
The adapter method that is being called:
fun replaceLastFragmentWith(newFragment: Fragment) {
fragmentList[LAST_FRAGMENT_INDEX] = newFragment
notifyItemChanged(LAST_FRAGMENT_INDEX)
}
If I click a button inside PlaylistMenuFragment that goes to PlaylistContentFragment, executing the methods above, everything works fine. But if then I click a button inside PlaylistContentFragment that goes back to PlaylistMenuFragment, the app crashes, throwing the following exception:
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.viewpager2.widget.ViewPager2$RecyclerViewImpl{8ebfd7a VFED..... ......ID 0,0-720,1040 #1}, adapter:com.pegoraro.musicast.main.PagerAdapter#a12492b, layout:androidx.viewpager2.widget.ViewPager2$LinearLayoutManagerImpl#e567188, context:com.pegoraro.musicast.main.MainActivity#e8e6424
The crash traces back to the methods above. If I wrap the notifyItemChanged inside the adapter method with a try-catch block, the app works as intended, with no sign of problem, unless I look in the logcat, which still shows the exception being thrown. It is a dirty fix, which I don't like:
fun replaceLastFragmentWith(newFragment: Fragment) {
fragmentList[LAST_FRAGMENT_INDEX] = newFragment
try {
notifyItemChanged(LAST_FRAGMENT_INDEX)
} catch (e: Exception) {
Log.d("TEST", e.toString())
}
}
I'm trying to implement a sort of navigation between these two Fragments inside a TabLayout, and this is the way I managed to do. So if the error comes from a fundamental problem of how I am approaching this issue, what is an alternative? And if not, what could be the cause of the problem? Thank you.
Calling any of notifyXX() methods on the ViewPager2 adapter is the same as that of the RecyclerView, because ViewPager2 internally functions based on RecyclerView.
And since notifyXX() methods work in background thread, and in your case this directly affects one of the ViewPager current fragments; so you need to explicitly do this in UI thread
viewpager.post {
notifyItemChanged(LAST_FRAGMENT_INDEX)
}

Error Fragment not attached to an activity when I leave a fragment and enter it again

When I change the fragment and return to the one that was at the beginning, running a line I get the error Fragment not attached to an activity. It is strange because before I get to this line, I use requireAcitivity() and it works fine. The error appears after calling an ArrayAdapter custom
When I call the arrayadapter, the requiredActivity method still works:
val adapter = ListUserGameInfoAdapter(requireContext(), gameViewModel!!)
gameInfoListUsers.adapter = adapter
Each row of the list created by the arrayAdapter has a button.
icon.setOnClickListener {
gameViewModel.userToDeleteFromGamePositionLiveData.value = position
}
The button changes the value of a LiveData of the viewModel. By using an observer I pick up this new value of the LiveData. Inside the code of the observer, if I call requiredActivity(), the following error appears
val userToDeleteObserver = Observer<Int> {
if (it != null) {
//If I call requiredActivity here, I get the error Fragment not attached to an activity
showDialog(it)
}
}
gameViewModel!!.userToDeleteFromGamePositionLiveData.observe(
requireActivity(),
userToDeleteObserver
)
The strange thing is that the first time I enter this fragment the error does not appear, only when I go to another one and come back to this one.
You're adding an observer in your fragment, but tying it to the lifecycle of your activity. Therefore it will keep observing for changes even when the fragment has been destroyed, which is why you're getting a crash when calling requireActivity() in your observer.
If you do some debugging, you'll probably notice that the observer is actually triggering twice, once for the old fragment (no longer attached to an activity) and once for the new fragment.
You should be using Fragment.getViewLifecycleOwner() instead.

Androidx Navigation IllegalStateException after onSaveInstanceState

I have an app using AndroidX's Navigation library, but I'm getting odd behavior. Particularly around my app going in/out of the background. Here are two examples:
In a simple on click listener in a Fragment I have:
(Kotlin)
button.setOnClickListener {
findNavController().popBackStack()
}
From this, I see crashes saying it threw an IllegalStateException since it ran after onSaveInstanceState.
I have a ViewModel associated with my Fragment and I register my observers to the fragment view's lifecycle. This means that I get notified during onStart. Some key events, such as login state determine the app's navigation. In my case I have a splash screen that could go to either a login screen or the main screen. Once a user completes login, I reset the navigation (taking me back to the splash screen). Now the auth state is ready and I want to navigate to the main fragment, this throws an error often because onResume must be called before the FragmentManager is considered ready. I get an error saying I'm in the middle of a transaction and I can't add a new one. To mediate this I had to write this strange bit of code:
(Kotlin)
private fun safeNavigateToMain() {
if (fragmentManager == null) {
return
}
if (!isResumed) {
view?.post { safeNavigateToMain() }
return
}
try {
findNavController().navigate(R.id.main)
} catch (tr: Throwable) {
view?.post { safeNavigateToMain() }
}
}
Does anyone know how I can get the navigation controller to play nice with the fragment lifecycles without having to add these workarounds?
As per the Navigation 1.0.0-alpha03 release notes:
FragmentNavigator now ignores navigation operations after FragmentManager has saved state, avoiding “Can not perform this action after onSaveInstanceState” exceptions b/110987825
So upgrading to alpha03 should remove this error.

Categories

Resources