I'm using a shared ViewModel in Navigation component rather than creating a ViewModel for every fragment (mostly because it's easier) but now I have a problem when I re-enter a fragment and subscribe to the ViewModel live data of that fragment, I get the last state also too.
here is the ViewModel Code:
val apiLessonData: MutableLiveData<String>> = MutableLiveData()
fun getLessonsUserCreated() =
apiCall(MyMaybeObserver(apiLessonData))
in MyMaybeObserver, I have somthing like this:
override fun onSuccess(t: T) {
apiDataObserver.postValue(t)
}
and this is how I observe it in my fragment:
private val apiAddGoalData = Observer<String> { response ->
showSnack(response)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
.
.
viewModel.apiAddGoalData.observe(viewLifecycleOwner, apiAddGoalData)
.
.
}
now when I enter the first time it works fine but I open it the second time, it shows the snack from the previous time, how to stop this without creating new ViewModel?
In the simple way You could set null for your MutableLiveData after getting data in onchange method of the observer. For more information you can read this article:livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case
. also you can see this question maybe help you: How to clear LiveData stored value?
I don't think your problem is with the LiveData since you are wisely using the viewLifecycleOwner, problem is with the state of the view and lifeCycle of the fragment. With navigation component of jetpack, fragments get replaced in the container. Think of this scenario: You open fragment A then you navigate to frament B and press back button to return to fragment A. onCreateView and onViewCreated methods of the fragment A gets called again. Since the onDestroy of fragment A haven't been called when you opened fragment B some of the view states will be restored while returning to A. This is as you might know the same reason we use viewLifecycleOwner. So Nullify or clear the state of the views in the onDestroyView of the fragment A:
recyclerView.setAdapter(null)
checkBox.setChecked(false)
Related
Scenario: I have 2 fragments ProductList and ProductDetail in my nav graph. And when i click on any product it opens the ProductDetail fragment using findNavController.navigate() method.
Problem: The problem is when I go back from ProductDetail to ProductList fragment, the ProductList fragment reloads again and it also calls the api to fetch products list, which I want to avoid.
If anyone knows the reason behind it or the solution to this particular issue please let me know in comments.
You should cache the fetch result locally, pull from the cache either during a fetch attempt or upon failure of a fetch attempt. This is a very common pattern on mobile.
Finally I got the answer from somewhere, so I am posting the sample code to resolve the issue I am facing -
private lateinit var contentView: View
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
if (!::contentView.isInitialized) {
binding = FragmentNewProductListBinding.inflate(layoutInflater)
contentView = binding.root
// initialize your views or set click listeners or call apis
}
return contentView
}
Note: To elaborate, there is some internal bug in the navigation library where it draws the previous fragment from scratch when going back from another fragment. So as a workaround or temporary patch, what we can do is just check whether the view is already initialized or not, if yes then don't create it again.
Hope you understand the reasoning and answer.
I have a code like this:
private val appViewModel: AppViewModel by activityViewModels()
private lateinit var user: User
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This sets the variable user to the value collected from a StateFlow from appViewmodel
lifecycleScope.launchWhenCreated {
appViewModel.user.collect { flowUser -> user = flowUser }
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// This method utilizes the lateinit user variable
lifecycleScope.launchWhenStarted {
doThingWithUser()
}
return binding?.root
}
Since the value of StateFlow persists even after being collected, after the screen rotates the first lifecycleScope.launchWhenCreated gets called, collects the flowUser from the flow again, assigns it to the lateinit user variable, and doThingWithUser gets called later and everything works fine.
But after two or more rotations, this stops being the case, for some reason user doesn't get initialized, doThingWithUser gets called and the app crashes with kotlin.UninitializedPropertyAccessException.
What am I doing wrong? Does the value from StateFlow vanishes after two collections/screen rotations? Something happens with the actual flow inside the ViewModel? Something happens with the onCreate and onCreateView methods? Or does launchWhenStarted and launchWhenCreated behave differently after two rotations?
Thanks.
I found out what the problem was. Apparently the Navigation Component messes with the order of the fragment's lifecycles, as seen here.
So when the screen rotated, due to the backstack order, the Navigation was creating another fragment that also interacts with the StateFlow of the ViewModel before the current Fragment. So, the other fragment onCreate method was sending something else to the flow, and therefore messing my current fragment collection.
The solution is to either make the flow collection independent of the fragment lifecycle, or change the collection in either one of the trouble fragments.
I am using Navigation library and my use case is preserve Fragment state on back press which I achieve by returning already inflated binding in onViewCreated as when changing fragments Navigations seems not to destroy already existing instance of this fragment the actual view variable exists when you navigate there back or up.
But I also have a use case when I need to recreate this Fragment instance so I expect to have a way to call onDestroy() for that fragment. But I don't see any api for removing/obtaining existing in the backstack instances.
So my question is how to get an existing instance of a Fragment from nav back stack and destroy it or just remove it by calling nav controller api.
some code:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProviders.of(requireActivity(), mViewModelFactory)
.get(MainViewModel::class.java)
parseNavigationExtra()
return if (::mBinding.isInitialized) {
mBinding.root
} else {
//create new binding
}
so when I call this action I still get the old binding root as the variable is still present.
<action
android:id="#+id/clearBackStack"
app:destination="#+id/mainFragment"
app:launchSingleTop="true"
app:popUpTo="#+id/mobile_navigation"
app:popUpToInclusive="true" />
List<Fragment> fragments = getActivity().getSupportFragmentManager().getFragments();
Fragment lastFragment = fragments.get(fragments.size() - 1);
getActivity().getSupportFragmentManager().beginTransaction().remove(emptyDialog);
changes in the navigation library since 2.1.0
NavBackStackEntry: You can now call NavController.getBackStackEntry(), passing in the ID of a destination or navigation graph on the back stack. The returned NavBackStackEntry provides a Navigation-driven LifecycleOwner, ViewModelStoreOwner (the same returned by NavController.getViewModelStoreOwner()), and SavedStateRegistryOwner, in addition to providing the arguments used to start that destination.
So the plan is to use the new api to see what is available for NavBackStackEntry.
I am making an application using the Android Navigation component. But I ran into a very fundamental problem which can cause problems in the whole development of my application.
The Scenario
I have this Fragment where in onViewCreated I am observing a field from my viewmodel.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(EventDetailsViewModel::class.java)
viewModel.init(context!!,eventId)
viewModel.onEventDetailsUpdated().observe(this, Observer {
setEventDetails(it)
})
}
And in the setEventDetails method, I set recyclerviews with the data.
The PROBLEM
This fragment is a long fragment with a scroll. Suppose I scroll long way down to a section and click on a button which takes me to another fragment.
But when I come back to this fragment, it again takes me to the top and does everything that it did on first load.
That can be troubling. It is kind of recreating the whole fragment instead of keeping its old state.
What I tried
I searched a lot of questions. And went through This Github Query, This SO question, Another Git... But I could not solve my problem.
Please help, Thanks in advance.
Yes, Fragment's view will get destroyed whenever you navigate forward to another fragment.
RecyclerView's scroll position should be automatically restored, even when new instance of RecyclerView is created and new Adapter instance is set, as long as you setup everything with the same dataset as before. Also, you need to do it before the first layout pass.
This means that you need your old data and you need to have it ready immediately (no async loads!).
ViewModelProvider should return the same ViewModel instance. That ViewModel holds the data you should be able to synchronously get and display on the UI. Make sure to refactor your viewModel.init method - you don't want to make API call if data is already there in case when going back. A simple boolean isInitialized can work here, or you can even check if LiveData is empty or not.
Also, you have a subtle bug when calling observe on LiveData. onViewCreated can be called many times for the same fragment (each time you navigate forward and back!) - so observe will be called each time. Your Fragment will be subscribed many times to the same LiveData. This means you will get events multiple times (once for each subscription). This can cause issues with RecyclerView state restoration too. Your subscription is tied to Lifecycle owner you passed. You passed Fragment's Lifecycle owner which is tied to Fragment's lifecycle. What you want to do is pass Fragment view's lifecycle owner, so whenever the view is destroyed the subscription gets cleared, and you only have 1 subscription ever and only while the Fragment's view is alive. For this, you can use getViewLifecycleOwner instead of this.
You need to rely on ViewModel to restore the fragment state because ViewModel doesn't get destroyed on fragment change.
In your viewModel, create a variable listState
class HomeViewModel : ViewModel() {
var listState: Parcelable? = null
}
Then in your fragment use below code
class HomeFragment : Fragment() {
private val viewModel by navGraphViewModels<HomeViewModel>(R.id.mobile_navigation)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (viewModel.listState != null) {
list.layoutManager?.onRestoreInstanceState(viewModel.listState)
viewModel.listState = null
}else{
//load data normally
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.listState = list.layoutManager?.onSaveInstanceState()
}
}
You don't have to initialize the view model each time. Just check for null before initializing. Don't know kotlin, still it will be something like:
if(viewModel == null){
viewModel = ViewModelProviders.of(this).get(EventDetailsViewModel::class.java)
viewModel.init(context!!,eventId)
}
try putting this code where you first call your fragment.
ft = fm.beginTransaction();
ft.replace(R.id.main_fragment, yourSearchFragment, "searchFragment");
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();
and this when going back to the fragment
ft = fm.beginTransaction();
ft.hide(getFragmentManager().findFragmentByTag("searchFragment"));
ft.add(R.id.main_fragment, yourDetailfragment);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();
I have been trying out Navigation Component for a while now but somehow not getting the reason (or explanation) behind current Lifecycle of Navigation Component. Here are some points that needs clarification.
1. Fragment to Fragment flow
In navigation Component every fragment (or lets say page) is recreated every time it is visited (or revisited). So, when you are at A and go to B, A is destroyed and later when you go back to A (like pressing back button) A is created from stretch.
In a traditional Activity patterns when you go back to A it just goes to onResume as it wasn't destroyed when moving to B. Any reason that this pattern is changed in Navigation Component?
The problem of recreating is when you have a lot of data and it takes time to get redrawn and it feels like app is freezing. An example would be simple HomePage (say Facebook NewsFeed). Preserving data can be handled with ViewModel but drawing of all of the feeds again require time and it will freeze.
There is another problem that recreation generates. Assume this scenario: A has an Collapsing AppBar with a NestedScrollView. User scrolls down and AppBar will collapse and then user moves to a different page B. When he comes back to A it will be redrawn and AppBar is expanded. I am not sure if it is a bug or I should do something to fix it? So any solution here is also welcomed.
2. Activity recreation
If activity is recreated for certain reasons like orientation change, every page in Navigation Component gets recreated until current destination. Although onCreate method of Fragment not onCreateView is getting called, I don't see a point of creating Fragments in Activity recreation. Any explanation would be welcomed.
Navigation component only supports fragment replacement as of now. So you won't be able to add() a fragment as you do it with Manual fragment transaction.
However, if your worry is about re-inflating the layout and re-fetching the data for the fragment, it could be easily resolved with below two methods.
Once the view is created, store it in a variable and use it whenever onCreateView() is called.
private var view: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (view == null) {
view = inflater.inflate(R.layout.fragment_list, container, false)
//...
}
return view
}
Source: https://twitter.com/ianhlake/status/1103522856535638016
Use ViewModel with the Fragment and hold the data required as a member variable. By this way, the data is not cleared when you replace the associated fragment. The ViewModel gets cleared only on onDestroy() of the fragment, which will only happen when you destroy the parent activity. https://developer.android.com/images/topic/libraries/architecture/viewmodel-lifecycle.png
The way that we use fragments to bridge data and views has changed slightly, and in a good way, when migrating to the Navigation library. It forces us to distinguish between Fragment and View lifecycles.
Pre-navigation: observe LiveData in onCreate() using Fragment's lifecycleScope.
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
...
import kotlinx.android.synthetic.main.old_fragment.*
class OldFragment : Fragment(R.layout.old_fragment) {
private val vm by activityViewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
vm.getLiveData().observe(this) { data ->
oldTextView.text = data.name
}
}
}
Navigation: observe LiveData in onViewCreated() using viewLifecycleOwner scope.
...
class NewFragment : Fragment() {
private val vm by activityViewModels<MainViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.getLiveData().observe(viewLifecycleOwner) { data ->
oldTextView.text = data.name
}
}
}
Key Notes:
Not all Lifecycle Owners are the same. The fragment lifecycle will not execute the observer when the view is recreated (while using Navigation library and navigating up/back).
The viewLifecycleOwner cannot be accessed before the view is created.
Hopefully, this can help prevent replacement of LiveData code as developers migrate to Navigation.