Android Why Fragment save viewState by itself? - android

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.

Related

How to keep a Mutable List with the previous data added in a view Model class in Kotlin?

I have two classes: one is the viewModel (ShoesViewMode.ktl) to keep the data and the other is the Fragment to show the data.(ShoesList.kt )
ShoesList has a mutableList of words and I recover it from the ShoesList to show in a scrollview.
I get a new word from an EditText from a Fragment -> Click on Save button -> Pass this word through nave Args to ShoesDetails -> save it in the ShoesViewModel -> Recover it and show in the Fragment.
The problem is that every time I add a new word, the list doesn't keep the last one added. It's like if the mutableList was always recreated.
I would like to go back the screen and add a new word, and a new word and see the previous words added in the list.
How can I keep the words added previously?
ShoesViewModel.kt
class ShoesViewModel(_newShoe: String?=null): ViewModel() {
private var _shoesList = MutableLiveData<MutableList<String>>()
init {
//receives the score when the class is instanciated
_shoesList.value = mutableListOf(
"trade",
"calendar",
"sad",
"desk",
"guitar",
"home",
"railway",
"zebra",
"jelly",
"car",
"crow",
"trade",
"bag",
"roll"
)
}
val shoesList: LiveData<MutableList<String>>
get() = _shoesList
fun save (newShoe: String){
_shoesList.value?.add(newShoe)
}
ShoesList. kt // FRAGMENT to show data
val shoesListArgs by navArgs()
viewModelFactory = ShoeViewModelFactory(shoesListArgs.newShoe)
viewModel = ViewModelProvider(this, viewModelFactory).get(ShoesViewModel::class.java)
//get the view Model //pass to the variable in the xml
binding.shoesViewModel = viewModel
binding.setLifecycleOwner(this)
viewModel.save(shoesListArgs.newShoe) //save new Shoe to the List
//keeps track of shoesList. This is an OBSERVER
viewModel.shoesList.observe(viewLifecycleOwner, Observer{ shoesList ->
loadShoes(shoesList)
})
//actig to floating button
binding.buttonFloating.setOnClickListener{ view:View ->
view.findNavController().navigate(ShoesListDirections.actionShoesListToShoesDetails())
}
return binding.root
}
private fun loadShoes(list:MutableList<String>){
for(shoe in list){
val newTextViewShoe = TextView(context)
newTextViewShoe.text = shoe // add TextView to LinearLayout
binding.linearlayoutShoelist.addView(newTextViewShoe)
}
}
}
I save a new word, the Fragment changes and list shows the new word. When I go back to the screen to save a new word, it saves the new word, but the previous on disappears.
In method save You need:
fun save(newShoe: String) {
if (shoeList.value.isNullOrEmpty){
shoeList.value = mutableListOf(newShoe)
}
else {
shoesList.value = shoesList.value.add(newShoe)
}
}
Your problem is that You are trying to set data to the list rather than livedata by calling livedata.value.add(). Your value here is getValue() method, that does nothing but gives you value. If You need to update a value in livedata, then You go:
liveData.value = newValue
Whether this means setValue() method. Additionally, if You want to set data from another thread than main, use postValue():
liveData.postValue(newValue)

PagingDataAdapter stops loading after fragment is removed and added back

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:

Android FragmentManager and Fragment Result API

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.

FragmentStateAdapter doesn't show the fragments with arguments after recycling

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()

Only allow one instance when navigate with NavController

I'm currently using Android Navigation Architecture in my project. It has a feature that can launch any fragment with a shortcut. Currently I'm using NavController to navigate to desired destination when clicking at a shortcut.
But when I clicked a shortcuts with multiple times, every time a new instance of the fragment will be created.
So, my question is, Is there any way to only accept one instance of a fragment when navigate to it with NavController?
I'm googling many times but found nothing. Thanks in advance.
Add a check before navigating to the destination as it would not add a new instance.
class A: AppCompatActivity {
fun onCreate(...) {
// ...While navigating
if (navController.currentDestination?.id != desiredDestination?.id) {
navController.navigate(desiredDestination)
}
// ...else do nothing
}
}
Callback from NavController: https://developer.android.com/reference/androidx/navigation/NavController#addOnDestinationChangedListener(androidx.navigation.NavController.OnDestinationChangedListener)
You can use by navGraphViewModels delegate
The most important thing is to set id to your views in order to save state during config changes.This has not mentioned in official docs.
by default fragment navigation won't be saved during config changes(rotation and ...).
ViewModel will remain across config changes and you can save your state there then restore it.
Also check these links:
https://code.luasoftware.com/tutorials/android/android-jetpack-navigation-lost-state-after-navigation/
and
Android navigation component: how save fragment state
You can use safeOnClickListener instead of default onClickListener for capturing click on shortcut, so basically with safeOnClickListener you ignore all the click event for a given duration.
class SafeClickListener(
private var defaultInterval: Int = 2000,
private val onSafeCLick: (View) -> Unit
) : View.OnClickListener {
private var lastTimeClicked: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) {
return
}
lastTimeClicked = SystemClock.elapsedRealtime()
onSafeCLick(v)
}
}
fun View.setSafeOnClickListener(delay: Int = 2000, onSafeClick: (View) -> Unit) {
val safeClickListener = SafeClickListener(delay) {
onSafeClick(it)
}
setOnClickListener(safeClickListener)
}

Categories

Resources