Fragment Get Destroyed And Created Again Using Android Jetpack Navigation Controller - android

I have an activity with three fragments. It will navigate between fragments using navigation controller. But everytime i move to other fragment, the previous fragment destroyed.
When I back (using back key or app bar back button), it will called onCreateView again.
The problem is, I have a method called fetchProducts() that should run once when view created on fragment. Because the fragment alwasy get destroyed, so my fetchProducts always get called again and I dont wanna do that.
Im using viewBinding btw.
Here some of my code:
#AndroidEntryPoint
class HomeMainFragment : Fragment(R.layout.fragment_main_home) {
private var _binding: FragmentMainHomeBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeMainViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentMainHomeBinding.bind(view)
setupRecyclerView()
observe()
goToCreateProductPage()
fetchProducts()
}
//...
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
How to keep the fragment so it will not destroyed? Especially using viewBinding

As this issues:
Support multiple back stacks for Bottom tab navigation
You can use navigation library version 2.4.0-alpha04 and fragment version 1.4.0-alpha04 for back stacks support.
And yes you should consider using ViewModel to get data that you fetch.

Related

UninitializedPropertyAccessException: lateinit property binding has not been initialized

I am getting a random crash "lateinit property binding has not been initialized". Most of the time it's working fine but a few time randomly we are getting this crash on crashlytics.
Please let me know what's wrong here
I have a BaseActivity with following code
abstract class BaseActivity<D : ViewDataBinding> : AppComptActivity() {
abstract val layoutId: Int
lateinit val binding: D
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
binding = DataBindingUtil.setContentView(this, layoutId)
....
}
}
I have a HomeActivity which override BaseActivity with following code
class HomeActivity : BaseActivity<ActivityHomeBinding>() {
override val layoutId: Int get() = R.layout.activity_home
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
....
}
}
I am using bottomNavigation menu and one of the fragment is HomeFragment
class HomeFragment : BaseFragment<FragmenntHomeBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState:Bundle)
(activity as HomeActivity).binding.appBarHome.visible(false)
//HERE I AM GETTING lateinit property binding has not been initialized crash
}
}
I don't want to use isInitialized property of lateinit as this will not solve my issue
As mentioned in the comment, I'd suggest instead of calling parent container (Activity) objects directly, register a listener to a navigation change like this in HomeActivity:
navController.addOnDestinationChangedListener { controller, destination, arguments ->
if(destination.id = R.id.homeFragment) {
// TODO hide/show your view here
}
}
In that case, you are sure that the view gets hidden/shown when it should be without relying on the HomeFragment being only in HomeActivity as this can change in the future and your app will start crashing
If you have an orientation change or other config change, or the OS process is killed while in the background and the user returns to the app, Android will recreate the Activity and the Fragments.
Unfortunately, it creates the Fragments first, before creating the Activity. So you cannot rely on the existence of the Activity until the Fragment has been attached to the Activity. You should move code that relies on the existence of the Activity to
onActivityCreated().
Note: I also agree with the comment about not doing it this way. Your Fragment should not make assumptions like this (that it is hosted by HomeActivity), but instead should make some callback to the hosting Activity and let the hosting Activity set the visibility of the app bar (or whatever else it wants to do).

Use MutableStateFlow as Hot stream, Kotlin Android

I am migrating from LiveData to Flow and faced the following problem:
I have a flow in viewModel
class MyViewModel() : ViewModel() {
val state = MutableStateFlow<Boolean>(false)
}
class FirstFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launchWhenCreated {
viewModel.loginPresenterState.startVerifyFragmentEvent.collectLatest {
Log.d("Nurs", "loginPresenterState $it")
if (it)
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
}
}
}
when this flow is triggered , My FirstFragment navigates to another fragment "B".
But when I press back button, the state triggers one more time, and instead of navigating to FirstFragment, I am coming back to "B". I suppose this behavior is because Flow is Cold. How to manage it be called only once?
Probably because the states remain same and when you came back it re-observes state and navigates. Check this article and use the EventWrapper that mentioned in the article. He used livedata but same logic applies for stateflow too. article

Does an instance of a SharedViewmodel never dies?

I have an app that has a main activity and fragments depend on it, so this is normal.
Now, two of my 10 fragments need to communicate, which I use the example given here
https://developer.android.com/topic/libraries/architecture/viewmodel.html#sharing
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ? Because now I dont need anymore that viewmodel instance, but as per documentation says, this instance will be retained from the Activity that contains these fragments
This is not what I'm looking for to communicate between fragments since now a new instance of that viewmodel will be the same as the past one I have created, I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
Now, if MasterFragment and DetailFragment dies (both does a popBackStack()) does that instance of the viewmodel keep active untill I finish the MainActivity containing this Fragments ?
Correct. While it so happens that only two of your fragments use it, that ViewModel is scoped to the activity.
I mean, it will reuse the instance that I used with the already poped fragments, in which I will need to extra handling a deletion or reset of all the data inside this viewmodel instead of getting a new fresh viewmodel.
Then perhaps you should not be using activityViewModels(). For example, you could isolate these two fragments into a nested navigation graph and set up a viewmodel scoped to that graph.
Does it works this way or that instance automatically dies when no fragments depending on it are in the stack anymore ?
The ViewModel system does not know about what is or is not "depending on it". It is all based on the ViewModelStore and the ViewModelStoreOwner that supplies it. activityViewModels() uses the activity as the ViewModelStoreOwner, so viewmodels in that ViewModelStore are tied to the activity.

How can I pass data to the first Fragment whilst using the Navigation Architecture?

I'm trying to pass a bundle of object instances down from my main activity to the first fragment in a chain of other fragments using the NavHostFragment. I've tried all sorts but the bundle always seems to be null once it reaches the first fragment.
Here's how I'm initiating the NavHostFragment (frameContainer is a Frame Container element in my layout xml)
NavHostFragment navHost = NavHostFragment.create(R.navigation.claim_nav_graph);
getSupportFragmentManager().beginTransaction()
.replace(R.id.frameContainer, navHost)
.setPrimaryNavigationFragment(navHost)
.commit();
The documentation says there are 2 different .create functions, one of them you can pass a second arguments to as a bundle, but Android Studio doesn't allow me to use this version.
Does anyone have any ideas?
Thanks in advance!
It does seem to be a flaw with the NavHostFragment, passing data down to the first fragment does not seem to be possible, as the Bundle you can set as a second argument on the create function is overwritten along the way.
In the end I resolved this by building the bundle in the first fragment of the activity instead. I was able to access the activities intent properties using the below.
// Kotlin
activity.intent?.extras?.getBundle(KEY_BUNDLE_ID)
// Java
getActivity().getIntent().getBundleExtra(KEY_BUNDLE_ID)
This was enough of a workaround for me in this situation, but it would be great if it was possible
If you're using viewModels, you can do this:
your viewmodel:
class NiceViewModel: ViewModel() {
var dataYouNeedToPass = "initialValue"
}
your activity:
class MainActivity : AppCompatActivity() {
val niceViewModel: NiceViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
niceViewModel.dataYouNeedToPass = "data You Need To Pass"
}
}
your fragment:
class YourFragment : Fragment() {
private lateinit var niceViewModel: NiceViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
niceViewModel = (activity as MainActivity).niceViewModel
niceViewModel.dataYouNeedToPass //do whatever you need to do with this
}
}

Multiple LiveData Observers After Popping Fragment

Issue
Summary: Multiple LiveData Observers are being triggered in a Fragment after navigating to a new Fragment, popping the new Fragment, and returning to the original Fragment.
Details: The architecture consists of MainActivity that hosts a HomeFragment as the start destination in the MainActivity's navigation graph. Within HomeFragment is a programmatically inflated PriceGraphFragment. The HomeFragment is using the navigation component to launch a new child Fragment ProfileFragment. On back press the ProfileFragment is popped and the app returns to the HomeFragment hosting the PriceGraphFragment. The PriceGraphFragment is where the Observer is being called multiple times.
I'm logging the hashcode of the HashMap being emitted by the Observer and it is showing 2 unique hashcodes when I go to the profile Fragment, pop the profile Fragment, and return to the price Fragment. This is opposed to the one hashcode seen from the HashMap when I rotate the screen without launching the profile Fragment.
Implementation
Navigation component to launch new ProfileFragment within HomeFragment.
view.setOnClickListener(Navigation.createNavigateOnClickListener(
R.id.action_homeFragment_to_profileFragment, null))
ViewModel creation in Fragment (PriceGraphFragment). The ViewModel has been logged and the data that has multiple Observers only has data initialized in the ViewModel once.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
}
Listen for data from ViewModel in original Fragment (PriceGraphFragment). This is being called multiple times, however it is only expected to have one Observer when the Fragment is loaded.
priceViewModel.graphLiveData.observe(
this, Observer { priceGraphDataMap: HashMap<Exchange, PriceGraphLiveData>? ->
// This is being called multiple times.
})
Attempted Solutions
Creating the Fragment's ViewModel in the onCreate() method.
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
Creating the ViewModel using the Fragment's activity and the child Fragment's parent Fragment.
priceViewModel = ViewModelProviders.of(activity!!).get(PriceDataViewModel::class.java)
priceViewModel = ViewModelProviders.of(parentFragment!!).get(PriceDataViewModel::class.java)
Moving methods that create the Observers to the Fragment's onCreate() and onActivityCreated() methods.
Using viewLifecycleOwner instead of this for the LifecycleOwner in the method observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer).
Storing the HashMap data that is showing as duplicates in the ViewModel opposed to the Fragment.
Launching the child Fragment using the ChildFragmentManager and the SupportFragmentManager (on the Activity level).
Similar Issues and Proposed Solutions
https://github.com/googlesamples/android-architecture-components/issues/47
https://medium.com/#BladeCoder/architecture-components-pitfalls-part-1-9300dd969808
https://plus.google.com/109072532559844610756/posts/Mn9SpcA5cHz
Next Steps
Perhaps the issue is related to creating the nested ChildFragment (PriceGraphFragment) in the ParentFragment's (HomeFragment) onViewCreated()?
ParentFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
user = viewModel.getCurrentUser()
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.replace(binding.priceDataContainer.id,
PriceGraphFragment.newInstance())
?.commit()
}
Test replacing the LiveData objects with RxJava observables.
This is basically a bug in the architecture. You can read more about it here. You can solve it by using getViewLifecycleOwner instead of this in the observer.
Like this:
mViewModel.methodToObserve().observe(getViewLifecycleOwner(), new Observer<Type>() {
#Override
public void onChanged(#Nullable Type variable) {
And put this code in onActivityCreated() as the use of getViewLifecycleOwner requires a view.
First off, thank you to everyone who posted here. It was a combination of your advice and pointers that helped me solve this bug over the past 5 days as there were multiple issues involved.
Issues Resolved
Creating nested Fragments properly in parent Fragment (HomeFragment).
Before:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.add(binding.priceDataContainer.id, PriceGraphFragment.newInstance())
?.commit()
fragmentManager
?.beginTransaction()
?.add(binding.contentFeedContainer.id, ContentFeedFragment.newInstance())
?.commit()
}
...
}
After:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null
&& childFragmentManager.findFragmentByTag(PRICEGRAPH_FRAGMENT_TAG) == null
&& childFragmentManager.findFragmentByTag(CONTENTFEED_FRAGMENT_TAG) == null) {
childFragmentManager.beginTransaction()
.replace(priceDataContainer.id, PriceGraphFragment.newInstance(),
PRICEGRAPH_FRAGMENT_TAG)
.commit()
childFragmentManager.beginTransaction()
.replace(contentFeedContainer.id, ContentFeedFragment.newInstance(),
CONTENTFEED_FRAGMENT_TAG)
.commit()
}
...
}
Creating ViewModels in onCreate() as opposed to onCreateView() for both the parent and child Fragments.
Initializing request for data (Firebase Firestore query) data of child Fragment (PriceFragment) in onCreate() rather than onViewCreated() but still doing so only when saveInstanceState is null.
Non Factors
A couple items were suggested but turned out to not have an impact in solving this bug.
Creating Observers in onActivityCreated(). I'm keeping mine in onViewCreated() of the child Fragment (PriceFragment).
Using viewLifecycleOwner in the Observer creation. I was using the child Fragment (PriceFragment)'s this before. Even though viewLifecycleOwner does not impact this bug it seems to be best practice overall so I'm keeping this new implementation.
It's better to initialize the view model and observe live data objects in onCreate.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyFragmentViewModel::class.java)
// 'viewLifecycleOwner' is not available here, so use 'this'
viewModel.myLiveData.observe(this) {
// Do something
}
}
However, no matter where you initialize the view model, whether in onCreate or onViewCreated, it will still give you the same view model object as it's created only once for the lifecycle of the Fragment.
The important part is observing the live data in onCreate. Because onCreate is called only on fragment creation, you're calling observe only once.
onViewCreated is called both when the fragment is created and when it is brought back from the back stack (after popping the fragment on top of it). If you observe live data in onViewCreated it will get the existing data that your live data is holding from the previous call immediately on returning from the back stack.
Instead, use onViewCreated only to fetch data from the view model. So whenever the fragment appears, either on the creation or returning from the back stack, it will always fetch the latest data.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchData()
...
}

Categories

Resources