how to make jetpack compose lifecycle compatible with Fragment life cycles? - android

My app previous was made using Fragments as pages. Now i'm integrating Jetpack Compose into my app. The pages are like this:
HomePageFragment -> the home page made with traditional XML
ItemListPageFragment -> a fragment wraps ComposeView that everything is done by Compose
ItemDetailPageFragment -> item detail page made with traditional XML
Here are the effects I want to achieve:
When entering ItemListPage, show loading then a list of items. then user scroll down a bit, select an item, jump to ItemDetailPage
When go back from ItemDetailPage to ItemListPage, UI shouldn't reload, but stays at where it was left with.
My ItemListPage is like this:
#Composable
fun Content(){
// third party lib that kind of like useSWR for React
val states = useRequest(fetcher = {....})
if(states.error) {...}
if(states.loading) {....}
// render list of items
....
}
right now it triggers fetch and loading UI when I go back from ItemDetailPage to ItemListPage.
I think the issue is Fragment destroy views when "page-out". Then when it is "back-to" page, it creates new ComposeView and triggered onLoad event.

Use ViewModel
OR
Use rememberSavable so that there won't be change even if there are any configuration changes

Related

Android navigation component display multiple DialogFragments in a row

Hi folks I need to throw up a series of DialogFragments one after another using the navigation component. I have encountered some pretty unusual system behavior which looks like a race condition. I show the dialogs with a global action after an item is clicked, and use the fragment result api to determine if another one should be shown.
I am using a custom layout so there's no positive / negative listeners etc., and my own continue / cancel buttons send a result back to the host fragment.
ItemsFragment.kt:
navController.navigate(RegisterItemsDirections.openModsDialog(item.id, 0))
fragment.setFragmentResultListener(ItemsFragment.MODIFIERS_REQUEST) { key, bundle ->
//kill the current dialog
navController.navigateUp()
//some logic to determine if we need another dialog...
if(needAnotherDialog){
//navigate to the next one
navController.navigate(RegisterItemsDirections.openModsDialog(item.id, lastModGroupSelectionIndex + 1))
}
}
ModsDialogFragment.kt, when the user clicks "continue"
setFragmentResult(MODIFIERS_REQUEST, bundleOf(MODIFIERS_RESULT to selectedMods))
So the issue only appears on 3rd or more dialogs on my devices, I can see that the 1st and 2nd dialogs are completely destroyed and detached. When it displays the 3rd one, the first one attaches itself again, and appears beneath the 3rd one which I can't explain.
I've tried:
Popping the back stack in the global action's nav xml
Navigating up or dismissing the dialog fragment in the dialog itself (before calling setFragmentResult), which is the most logical place to put it
Popping the backstack instead of navigating up, basically the same thing in this case
When I don't dismiss / nav up any of the dialogs and allow them all to just stack, there's no issue. When I delay the navigation by 500ms there is no issue. So navving up and then immediately navigating to another instance of the dialog are fighting with eachother producing very strange results. What could be the solution here that doesn't involve a random delay?
Navigation version is 2.3.3 and I'm having a lot of trouble trying to update it due to AGP upgrades being incompatible with a jar I need so I'm not sure if this has been fixed.
I figured out the issue, it's down to dumb copy pasting.
I took the donut tracker sample code and made it stack dialogs in the same way and there was no issue.
The difference between the two was I am subclassing DialogFragment and for some unknown reason doing this:
override fun show(manager: FragmentManager, tag: String?) {
val transaction = manager.beginTransaction()
val prevFragment = manager.findFragmentByTag(tag)
if (prevFragment != null) {
transaction.remove(prevFragment)
}
transaction.addToBackStack(null)
show(transaction, tag)
}
This code predates the Nav component library I believe, and it completely f***s with the fragment manager used by the navigation component. So the moral of the story is to not do bizarre things in super classes, or better yet not super class at all.

One-Single Activity or Two Activities

My app's main Activity (containing a NavigationDrawer) allows to navigate through many (20 aprox) Fragments, because of navDrawer item clicks and other views' clicks inside each fragment.
Then, it moves to a point where I need a BottomNavigationView (maintaining also the navDrawer). From this point, because of the bottomNavView and other views' clicks, I can move to other different 10-15 fragments, aprox, and also to the ones that the main NavigationDrawer allows, but, in case I move to a Fragment through a click on any main navDrawer's item, the bottomNavView should be hidden.
So, is it correct here to use a One-Single Activity approach and be controlling the visibility of the bottomNavView or shall I use Two Activities in order to avoid being pendent of this in all navigations?
I don't believe there's a "right or wrong" answer in this case.
It really boils down to how you want to architect your application, as long as you're consistent.
If your fragments have a "state" and a ViewMOdel and such, then a single activity swapping fragments while controlling its own state (when to show the bottom bar) may be simpler than having to maintain two different activities, since navigation is done always between fragments.
It will also be tied to how the backstack behaves in each case (so test accordingly to ensure you get the expected behavior).
Simple Idea (with a single act)
This is pseudo-code, not perfect, compiling, functional code.
class BottomBarUseCase() {
operator fun invoke(destination: String): Boolean =
when (destination) {
"A", "B", "C" -> true
else -> false
}
}
Your Activity's ViewModel (greatly simplified of course)
class XXXViewModel(
private val bottomBarUseCase: BottomBarUseCase
): ViewModel() {
private val _state = MutableLiveData<YourState>(YourState.Empty)
fun setupBottomBar(destination: String) {
if (bottomBarUseCase(destination)) {
_state.value = SomeState.ShowBar
} else {
_state.value = SomeState.HideBar
}
}
Your Activity observes the state and does what it needs to do.
There are ways to streamline this and what not, but essentially, you're delegating the responsibility to show the bar to the use-Case, which you can test in isolation to ensure it does what you want it to do (aka: don't show the bar for certain destinations).
Your Fragments don't care about any of this (unless they too, need to make the decision, in which case you can still inject the useCase in the Fragment's viewModels and ask there too, since the useCase doesn't have any special dependency).
That's what I would do, but without having to do this in real life, it's hard to visualize whether this would have other drawbacks.
In general, this is how I would approach a problem that needs to be resolved elsewhere in many places: isolating it.
Hope that clarifies it.

View not updating in OnClickListener

i am trying to make an E-Leaning app.
I have a submit button, that when pressed, sends the user's answer to a server to be evaluated.
During that evaluation, I want to display a loading indicator (I am simply using Android Studio's progress bar).
During the onViewCreated of my fragment, I set the onClickListener as follows:
submit_button.setOnClickListener {
submit_button.isClickable = false
loadingIndicator.visibility = View.VISIBLE
inputFragment.passDataToActivityAndEvaluate()
}
On press, a coroutine is launched that makes an html request. After that, I want to hide the loading spinner before navigating the child fragment
loadingIndicator.visibility = View.INVISIBLE
submit_button.isClickable = true
The idea sounded simple enough, however, I discovered that when trying to make any form of UI change in the OnClickListener, it only gets applied when the evaulation is already done and the app has navigated to the next fragment.

RecyclerView with StaggeredGridLayoutManager in ViewPager, arranges items automatically when going back to fragment

I am using Navigation component in my App, using google Advanced Sample(here).
my problem is when going back to a fragment, the scrolling position does not lost but it rearranges items and moves highest visible items so that top of those item align to top of recyclerview. please see this:
before going to next fragment:
and after back to fragment:
this problem is matter because some times clicked item goes down and not seen until scroll down.
how to prevent this behavior?
please consider:
this problem exist if using navigation component to change fragment. if start fragment using supportFragmentManager.beginTransaction() or start another activity and then go to this fragment it is OK. but if I navigate to another fragment using navigation component this problem is exist.(maybe because of recreating fragment)
also this problem exist if using fragment in ViewPager. i.e recyclerView is in a fragment that handle with ViewPagerAdapter and viewPager is in HomeFragment that opened with Navigation component. if recyclerView is in HomeFragment there is no problem.
no problem with LinearLayoutManager. only with StaggeredGridLayoutManager.
there is not difference if using ViewPager2 and also FragmentStatePagerAdapter
I try to prevent recreate of fragment(by this solution) but not solved.
UPDATE:
you can clone project with this problem from here
When using Navigation Component + ViewPager + StaggeredGridLayoutManager, wrong recyclerView.computeVerticalScrollOffset() has been returned during Fragment recreate.
In general, all layout managers bundled in the support library already know how to save and restore scroll position, but in this case, we had to take responsibility for this.
class TestFragment : Fragment(R.layout.fragment_test) {
private val testListAdapter: TestListAdapter by lazy {
TestListAdapter()
}
private var layoutManagerState: Parcelable? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postListView.apply {
layoutManager = StaggeredGridLayoutManager(
2, StaggeredGridLayoutManager.VERTICAL
).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
setHasFixedSize(true)
adapter = testListAdapter
}
testListAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT
}
override fun onPause() {
saveLayoutManagerState()
super.onPause()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
restoreLayoutManagerState()
}
private fun restoreLayoutManagerState () {
layoutManagerState?.let { postListView.layoutManager?.onRestoreInstanceState(it) }
}
private fun saveLayoutManagerState () {
layoutManagerState = postListView.layoutManager?.onSaveInstanceState()
}
}
Source code: https://github.com/dautovicharis/MyStaggeredListSample/tree/q_65539771
The Navigation Component behavior is normal when you navigate from one fragment to another. I mean, onDestroyView() method from the previous fragment is executed, so it means that your view is destroyed, but not the fragment. Remembering that fragment has two lifecycles one for the fragment and another one for the view, There was a video about it.
Also, there were issues registered in issue tracker in order to avoid this behavior in some cases and the GitHub issues:
https://issuetracker.google.com/issues/127932815
https://github.com/android/architecture-components-samples/issues/530
The problem is that when you have fragment that is heavy to recreate, is easier to do not destroy it and just add one fragment. So, when you go back it is not recreated. But, for this behavior is not part of navigation component.
Solutions
The easiest solution is to not use navigation component and work with the tradicional way, as you can see this works perfectly in you use case.
You can use the traditional way just for this use case, and use the navigation component for other cases.
You can inflate this view in an activity. So you are adding un activity
But if the previous tree options is not possible. You can try the following:
If you are using viewModel, you can use SaveState. Basically, it can save the data from your fragment, it is like a map data structure, so you can save positions from your list or recycler view. When go back to this fragment, get the position from this saveState object and use scrollToPosition method in order to add the real position.
Recycler view have methods for restore positions. You can see the uses cases for that, because first you need the data and then add the real position, for more details you can visit this link. This configuration for recycler view is useful also when you lose memory and you need to recreate the recycler view with asynchronous data.
Finally, if you want to understand more about how fragment works with navigation component, you can see this link

Android Jetpack Navigation Pass Lambda/Delegate between Fragments

I would like to pass a lambda from fragment A to fragment B when A transitions to B via a findNavController().navigate(R.id.action_a_to_b). The use case is B helps pick an item out to display on screen A.
Something like:
// In A
findNavController().navigate(R.id.action_a_to_b, configBlock: { fragmentB ->
fragmentB.itemSelectedCallback = this::itemSelected
})
I recognize this pattern doesn't quite fit with what Google is pushing (I assume they want shared observed view models with fragments not communicating between each other) but I am not looking to transition to that architecture style yet.
This is not yet possible, however, there is an existing feature request for being able to navigate for a result, which would let you get this type of functionality.

Categories

Resources