I have a Fragment A that displays a list of items. When one item is clicked we navigate to a Fragment B that shows a more detailed view on the selected item. So, I needed some sort of communication between Fragment A & B.
In the docs (see here ) they state that a shared ViewModel for communication between Fragments is the recommended way. So, implement it like this:
class SharedViewModel : ViewModel() {
// used to share Item between list and detail view
private val _selectedItem = MutableLiveData<Item>()
// Fragment B listens for this LiveData variable
val selectedItem : LiveData<Item> = _selectedItem
// called by the Fragment A which passes the selected item as argument
fun selectItem(item: Item) {
_selectedItem.value = item
}
}
In Fragment B, we listen for the selectedItem LiveData variable like this:
....
// I use a global variable here cuz I want to use it in several different methods
private lateinit var selectedItem : Item
....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// listen for the selected item
sharedViewModel.selectedItem.observe(viewLifecycleOwner, Observer { item ->
selectedItem = item
})
}
Now, in onStart() I have some operations that can only operate when selectedItem is initialized:
override fun onStart() {
super.onStart()
doSomethingWithSelectedItem(selectedItem) //<--- CRASH
doAnotherThingWithSelectedItem(selectedItem) // probably it would crash here, too
}
At this point, I get the exception that the selectedItem lateinit var variable is not initialized. So, I assume that at this point the setting in onViewCreated() (which is selectedItem = item) is not kicked in, yet.
So, my question is: Which lifecycle method is the best when you want to initialize a lateinit variable whose value is shared via a ViewModel between two Fragments ?
I think that the problem is that the function inside the observe() method of LiveData is not executed instantly. OnViewCreated() is a method that is executed previously to onStart(). If the LiveData observe() method executed the function instantly you wouldn't have any problem.
What the observe() method does is to set the LiveData object to observe to a change to its value without executing the defined function instantly.
What I would do is writting this piece of code in Fragment B:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
selectedItem = sharedViewModel.selectedItem.value
}
In your case I think that you don't even need to use a LiveData object because you only want to get a value and this value won't change as long as you are in the same fragment.
onStart() lifecycle of the fragment is called prior to the onViewCreated()
lifecycle of the fragment.
observe the livedata in onstart and set the lateinit variable. And depends on what ur function do, u can call them in onStart() after lateinit variable is set or in onViewCreated()
Related
The data in the recyclerview in my fragment uses the api of the website. But the data is lost when the bottom navigation is switched. But the data structure is not a simple int or string. How should I write it in onSaveInstanceState to store it. And how to make him restore the data type of LiveData<List> normally?
The data of the recyclerview looks like in the viewmodel.
private val _photos = MutableLiveData<List<SportData>>()
val photos: LiveData<List<SportData>> get() = _photos
data class
#Parcelize
data class SportData (
val GymID:Int,
val Photo1:String,
val Name:String,
val Address:String,
val OperationTel:String,
val OpenState:String,
val GymFuncList:String
): Parcelable
I try to save the data in onDestroyView() and fetch it in onViewCreated. It fails with null.
override fun onDestroyView() {
photos2=viewModel.photos
super.onDestroyView()
Log.d("aaa","destroyVIEW and ${photos2}")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d("aaa","=viewCreate and ${photos2}")
if (photos2!=null){
viewModel.saveData(photos2!!)
}
viewmodel
fun saveData(savePhoto:LiveData<List<SportData>>){
_photos=savePhoto as MutableLiveData<List<SportData>>
}
hello can you help me? thanks
Your data is already at ViewModel. You don't need to save it. Since data in your viewModel and viewModel lives while your aplication lives, you'll not lose it.
What might be happening is a reload when you go back to your list's fragment, right?
You are calling a viewModel method from your fragment. This method does the request.
What you need to do is to make sure your fragment won't call it if it doesn't need.
What you need to do is:
if (savedBundleState == null) { //Read this as android creating this frag for the very first time
//Here you call viewmodel method that does the request.
}
This is part 1
Since you are using Navigation Component, you'll need to setup it to avoid new fragments killing older fragments.
I am using AutoClearedValue class from this link and when view is destroyed, backing field becomes null and that is good but i have a thread(actually a kotlin coroutine) that after it is done, it accesses the value(which uses autoCleared) but if before it's Job is done i navigate to another fragment(view of this fragment is destroyed), then it tries to access the value, but since it is null i get an exception and therefore a crash.
what can i do about this?
also for which variables this autoCleared needs to be used? i use it for viewBinding and recyclerview adapters.
You have 2 option:
1- Cancelling all the running job(s) that may access to view after its destruction. override onDestroyView() to do it.
Also, you can launch the coroutines viewLifecycleOwner.lifecycleScope to canceling it self when view destroy.
viewLifecycleOwner.lifecycleScope.launch {
// do sth with view
}
2- (Preferred solution) Use Lifecycle aware components (e.g LiveData) between coroutines and view:
coroutines push the state or data in the live-data and you must observe it with viewLifeCycleOwner scope to update the view.
private val stateLiveData = MutableLiveData<String>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
stateLiveData.observe(viewLifecycleOwner) { value ->
binding.textView.text = value
}
}
private fun fetchSomething() {
lifecycleScope.launch {
delay(10_000)
stateLiveData.value = "Hello"
}
}
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.
According to the section "Share data between fragments" at https://developer.android.com/topic/libraries/architecture/viewmodel we are told that creating a ViewModel in the activity scope and sharing that amongst the fragments is the way to go.
This is the Fragment which sets the value in the ViewModel
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
This is the detail fragment which uses the property set
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
}
}
This is the ViewModel
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a button click, how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
Our DetailFragment will not be seeing the value since we were setting it on the button click in the MasterFragment. To understand the question better, consider we have Fragment A, B, C, and D and they share a ViewModel which has a value Fragment A B and C together computed and placed it in ViewModel for Fragment D to access.
Now when the system kills and recreates our application Fragment D won't have that value available.
OnSaveInstance also won't be able to help out much without resorting to dirty code. For simple situations, yes , but like the one in which FragmentA B and C together are making a value, in that situation, OnSaveInstance would be problematic.
OnSaveInstance should have been inside the ViewModel but alas I don't think that's the case. Any ideas?
ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting the ViewModel. The ViewModel remains in memory until the Lifecycle it's scoped to goes away permanently: in the case of an activity, when it finishes, while in the case of a fragment, when it's detached.
You can check it here
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a buttonClick , how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
You can't recover the value if the application is killed by the user or system or restarted.
To solve your purpose of accumulating data from Activity A, B and C and display it in Activity D even though the application is killed or restarted, you can choose any 1 method from the following:
1. SharedPreference
2. Local Database Room or SQLite
3. Store data in a file
I recommend you to use SharedPreference for small data and Room for Large and Complex data.
In a nutshell, ViewModel stores data temporary to survive orientation change(no need to write code of onSaveInstanceState and onRestoreInstanceState) and share data between Activities and Fragments. Data will be lost if the activity is destroyed or fragment is detached.
If you still want to get stored value after app reset or killed you need to save data to SharedPreferences or internal SqLite database and restore it after app start.
For those using Kotlin out there try the following approach:
Add the androidx ViewModel and LiveData libraries to your gradle file
Call your viewmodel inside the fragment like this:
class MainFragment : Fragment() {
private lateinit var viewModel: ViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// kotlin does not have a getActivity() built in method instead we use activity, which is null-safe
activity?.let {
viemModel = ViewModelProvider(it).get(SharedViewModel::class.java)
}
}
}
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()
...
}