How can be that a fragment F which uses the new Fragment Result API to get results from 2 other fragments: A, B gets the result from A but not from B because B has a different parent FragmentManager (and I don't know why) ? How could be something like that ? 2 fragments called in the same way but they end up having same Activity but different FragmentManager ? The function calls are the following:
//THIS DOESN'T WORK. THE LISTENER IS NOT CALLED AFTER THE RESULT IS SET
private fun navigateToItemLocation() {
setFragmentResultListener(REQUEST_LOCATION_KEY) { s: String, bundle: Bundle ->
val locationId = bundle.getParcelable<ParcelUuid>(LOCATION_ID)!!.uuid
viewModel.viewModelScope.launch(Dispatchers.IO) {
val location = LocationRepository().get(locationId)!!
changeItemLocation(location)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationPagerToNavStockLocationSelection()
findNavController().navigate(action)
}
//THIS WORKS FINE:
private fun navigateToItemDetails(item: Item2) {
setFragmentResultListener(SELECTED_ITEM_KEY) { s: String, bundle: Bundle ->
val propertySetId = bundle.getParcelable<ParcelUuid>(SELECTED_ITEM_SET_ID)!!.uuid
clearFragmentResultListener(SELECTED_ITEM_KEY)
viewModel.viewModelScope.launch(Dispatchers.IO) {
val repository = PropertySetRepository()
val propertySet = repository.get(propertySetId)!!
val propertySetInfo = ItemFactory.loadPropertySetInfo(propertySet)
withContext(Dispatchers.Main) { setPackageCode(null) }
selectItem(item.item, propertySetInfo, item.description, null)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationToNavStockItemDetails(ParcelUuid(item.item.id), true)
findNavController().navigate(action)
}
Both fragments A and B are in a separate Dynamic Feature. The only single problem I have is that when the following function is called:
fun onSelect() {
viewModel.pickedLocation.value = (viewModel.selectedLocation as? LocationExt2?)?.location
val result = bundleOf(Pair(LOCATION_ID, ParcelUuid(viewModel.pickedLocation.value!!.id)))
setFragmentResult(REQUEST_LOCATION_KEY, result)
findNavController().popBackStack()
}
setFragmentResult(REQUEST_LOCATION_KEY, result)
Doesn't produce any result because the FragmentManager is not the same of the calling Fragment. The same method in fragment A which is:
private fun onSetSelected(id: UUID) {
propertySets.removeObservers(viewLifecycleOwner)
adapter.tracker = null
setFragmentResult(SELECTED_ITEM_KEY, bundleOf(Pair(SELECTED_ITEM_SET_ID, ParcelUuid(id))))
findNavController().popBackStack()
}
As a temporarily workaround I replaced the call to Fragment's FragmentManager with Activity.supportFragmentManager.setFragmentResultListener. It works but still I do not understand why fragments A and B behave differently...
Check that fragment where you listen for fragment result and fragment where you set the result are in the same fragment manager.
Common case where this would happen is if you are using Activity.getSupportFragmentManager() or Fragment.getParentFragmentManager() alongside Fragment.getChildFragmentManager().
Check this blog article for the principles and rules with Fragment Result API on medium: https://medium.com/#FrederickKlyk/state-of-the-art-communication-between-fragments-and-their-activity-daa1fe4e014d
Only one listener can be registered for a specific request key.
If more than one listener is registered on the same key, the previous one will be replaced by the newest listener.
Related
I am having ListFragment, DetailFragment and OperationFragemnt. My ListFragment has the data list in Bundle object so it is always there when I am navigating from DetailFragment to ListFragment using
findNavController().popBackStack()
In OperationFragment my app is speaking with backend server and based on response I am trying to take user ListFragment using setFragmentResult as per the doc but can not get the setFragmentResultListener working
in OperationFragment
val resultToBeSent = "result"
parentFragmentManager.setFragmentResult("requestKey", bundleOf("bundleKey" to resultToBeSent))
in DetailFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
parentFragmentManager.setFragmentResultListener("requestKey",this) { requestKey, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
// Do something with the result
findNavController().popBackStack()
}
}
The result comes from the child fragment, everything else are NavArgs (there's two directions).
getParentFragmentManager().setFragmentResult("request_key", bundle)
Not sure if .popBackStack() may interfere. I use <dialog/> and dismiss(). I mean, the result already arrives before navigating. You cannot set this as listenener unless implementing interface FragmentResultListener:
/** Callback used to handle results passed between fragments. */
public fun onFragmentResult(requestKey: String, result: Bundle) {
if (requestKey.equals("request_key")) {
result.getString("bundle_key")
}
}
I am presenting a PagingSource returned by Room ORM on a PagingDataAdapter.
The RecyclerView is present on a Fragment -- I have two such fragments. When they are switched, they stop loading the items on next page and only placehodlers are shown on scrolling.
Please view these screen captures if it isn't clear what I mean--
When I scroll without switching fragments, all the items are loaded
When I switch Fragments before scrolling all the way down, the adapter stops loading new items
Relevant pieces of code (please ask if you would like to see some other part/file) -
The Fragment:
private lateinit var recyclerView: RecyclerView
private val recyclerAdapter = CustomersAdapter(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recycler_view)
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(context)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.customersFlow.collectLatest { pagingData ->
recyclerAdapter.submitData(pagingData)
}
}
}
View model-
class CustomersListViewModel(application: Application, private val debtOnly: Boolean): ViewModel() {
private val db = AppDatabase.instance(application)
private val customersDao = db.customersDao()
val customersFlow = Pager(PagingConfig(20)) {
if (debtOnly)
customersDao.getAllDebt()
else
customersDao.getAll()
}.flow.cachedIn(viewModelScope)
}
After I went through your code, I found the problem FragmentTransaction.replace function and flow.cachedIn(viewModelScope)
When the activity calls the replace fragment function, the CustomerFragment will be destroyed and its ViewModel will also be destroyed (the viewModel.onCleared() is triggered) so this time cachedIn(viewModelScope) is also invalid.
I have 3 solutions for you
Solution 1: Remove .cachedIn(viewModelScope)
Note that this is only a temporary solution and is not recommended.
Because of this, instances of fragments still exist on the activity but the fragments had destroyed (memory is still leaking).
Solution 2: Instead of using the FragmentTransaction.replace function in the Main activity, use the FragmentTransaction.add function:
It does not leak memory and can still use the cachedIn function. Should be used when the activity has few fragments and the fragment's view is not too complicated.
private fun switchNavigationFragment(navId: Int) {
when (navId) {
R.id.nav_customers -> {
switchFragment(allCustomersFragment, "Customer")
}
R.id.nav_debt -> {
switchFragment(debtCustomersFragment, "DebtCustomer")
}
}
}
private fun switchFragment(fragment: Fragment, tag: String) {
val existingFragment = supportFragmentManager.findFragmentByTag(tag)
supportFragmentManager.commit {
supportFragmentManager.fragments.forEach {
if (it.isVisible && it != fragment) {
hide(it)
}
}
if (existingFragment != fragment) {
add(R.id.fragment_container, fragment, tag)
.disallowAddToBackStack()
} else {
show(fragment)
}
}
}
Solution 3: Using with Navigation Component Jetpack
This is the safest solution.
It can be created using Android Studio's template or some of the following articles.
Navigation UI
A safer way to collect flows
I tried solution 1 and 2 and here is the result:
I am setting a navigation graph programmatically to set the start destination depending on some condition (for example, active session), but when I tested this with the "Don't keep activities" option enabled I faced the following bug.
When activity is just recreated and the app calls method NavController.setGraph, NavController forces restoring the Navigation back stack (from internal field mBackStackToRestore in onGraphCreated method) even if start destination is different than before so the user sees the wrong fragment.
Here is my MainActivity code:
class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
log("fresh start = ${savedInstanceState == null}")
navHost = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment
navController = navHost.navController
createGraph(App.instance.getValue())
}
private fun createGraph(bool: Boolean) {
Toast.makeText(this, "Is session active: $bool", Toast.LENGTH_SHORT).show()
log("one: ${R.id.fragment_one}, two: ${R.id.fragment_two}")
val graph =
if (bool) {
log("fragment one")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_one
}
} else {
log("fragment two")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_two
}
}
navController.setGraph(graph, null)
}
}
App code:
class App : Application() {
companion object {
lateinit var instance: App
}
private var someValue = true
override fun onCreate() {
super.onCreate()
instance = this
}
fun getValue(): Boolean {
val result = someValue
someValue = !someValue
return result
}
}
Fragment One and Two are just empty fragments.
How it looks like:
Repository with full code and more explanation available by link
My question: is it a Navigation library bug or I am doing something wrong? Maybe I am using a bad approach and there is a better one to achieve what I want?
As you tried in your repository, It comes from save/restoreInstanceState.
It means you set suit graph in onCreate via createGraph(App.instance.getValue()) and then fragmentManager in onRestoreInstanceState will override your configuration for NavHostFragment.
So you can set another another time the graph in onRestoreInstanceState. But it will not work because of this line and backstack is not empty. (I think this behavior may be a bug...)
Because of you're using a graph (R.navigation.nav_graph) for different situation and just change their startDestination, you can be sure after process death, used graph is your demand graph. So just override startDestination in onRestoreInstanceState.
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
if (codition) {
navController.graph.startDestination = R.id.fragment_one
} else {
navController.graph.startDestination = R.id.fragment_two
}
}
Looks like there is some wrong behaviour in the library and my approach wasn't 100% correct too. At least, there is the better one and it works well.
Because I am using the same graph and only changing the start destination, I can simply set that graph in onCreate of my activity and set some default start destination there. Then, in createGraph method, I can do the following:
// pop backStack while it is not empty
while (navController.currentBackStackEntry != null) {
navController.popBackStack()
}
// then just navigate to desired destination with additional arguments if needed
navController.navigate(destinationId, destinationBundle)
I am using fragment by supportFragmentManager
When I move to some Fragment and write down text in EditText, then go back(without any saving), and move again to that Fragment, EditText fill with I write down before.
I don't want to save the EditText state, but it save by itself....
Additionally, some Fragments don't save their viewState but other some Fragments save it.
The Fragment don't save their viewState are located first depth on Activity which means they are home screen at each tab.
On the other hand, The Fragment save their viewState by itself located second or more depth from first depth fragment(home screen)
This is FragmentNavigation code
class FragmentNavigation(private val activity: MainActivity) {
private val manager = activity.supportFragmentManager
private val fragmentMap = HashMap<Int, Stack<Fragment>>()
private var currentTab = 1 //This start destination fragment
private val container = R.id.nav_host_fragment
init { //I have 5 tabs
fragmentMap[0] = Stack<Fragment>()
fragmentMap[1] = Stack<Fragment>().apply { push(MemoFragment()) } //add starting point
fragmentMap[2] = Stack<Fragment>()
fragmentMap[3] = Stack<Fragment>()
fragmentMap[4] = Stack<Fragment>()
}
fun change(info: FragmentInfo) = //When user click bottomTab
fragmentMap[info.tag]?.let {
currentTab = info.tag
if (it.isEmpty()) {
it.push(info.fragment)
manager.commit {
replace(container, info.fragment)
}
return#let
}
//show last fragment I visited
manager.commit {
replace(container, it.last())
}
}
fun move(info: FragmentInfo) = //When user move to second or more depth fragment
fragmentMap[info.tag]?.let {
it.push(info.fragment)
manager.commit {
replace(container, info.fragment)
}
}
fun back() =
if(fragmentMap[currentTab]?.size == 1)
activity.finishApp()
else
popFragment()
private fun popFragment() =
fragmentMap[currentTab]?.let {
it.pop()
manager.commit {
replace(container, it.peek())
}
}
}
As above code say, first depth Fragments(don't save state) are shown by change function,
problematic fragment(save state) are shown by move function.
-> There is no onSaveInstanceState function in fragment.
-> Not using Bundle
-> I tried add and remove instead of replace as a fragmentTransaction, but same result..
-> tried remove all viewModel
-> Not using DataBinding
-> Tried Invalidate Cashes/Restart, Clean project, Rebuild Project many time.
-> remove and reinstall app so many time.
I'm trying to migrate to FragmentStateAdapter but got a problem with showing fragments.
When I first load the adapter, the createFragment() has been called for each Fragment. So from the beginning, I'm seeing the fragments with the correct data. But when I swipe back and forth - the first fragment start to be a blank white screen and if I do one more swipe again, the second page has an empty blank page too.
I tried to set a background color as a def param in the parent view to check is the fragment is still has been restored from the fragment manager. The result shows that yes - they are there but because I'm using arguments to declare the type that will be displayed in this fragment - after recycling the arguments are not exists anymore and that's why I see the blank white page.
Correct me if I'm wrong and what solution will fit better here?
Thanks
class ReviewContractAdapter(activity: FragmentActivity, private val cacheDirAbsolutePath: String):
FragmentStateAdapter(activity) {
companion object {
const val REVIEW_CONTRACT_NUMBER_OF_PAGES = 2
const val CONTRACT = "Contract"
const val PAD = "PAD"
}
override fun getItemCount(): Int = REVIEW_CONTRACT_NUMBER_OF_PAGES
override fun createFragment(position: Int): Fragment {
return PdfViewerFragment().apply {
arguments = Bundle().apply {
when (position) {
0 -> putString(CONTRACT_FILE_PATH, cacheDirAbsolutePath + CONTRACT_PATH)
else -> putString(PAD_FILE_PATH, cacheDirAbsolutePath + PAD_PATH)
}
}
}
}
}
Using Activity to init the Adapter
Nothing special there
review_contract_viewpager.adapter = adapter
TabLayoutMediator(review_contract_tabs, review_contract_viewpager) { tab, position ->
tab.text = when (position) {
0 -> ReviewContractAdapter.CONTRACT
else -> ReviewContractAdapter.PAD
}
}.attach()