sharing data between fragments via viewmodels - android

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

Related

How to save the data of recyclerview LiveData<List<SportData>> to avoid the loss of bottom navigation data?

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.

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 initialize an androidx ViewModel from parcelable data?

In my Android app, I pass custom data (UByteArray) from one activity to another using the parcelable interface.
I am using this data inside multiple fragments, so I rewrote the data class to extend androidx ViewModel and expose LiveData properties to the fragments. Now the UI updates are a lot nicer, but I think I am using it wrong because I overwrite all ViewModel values inside onCreate.
Now my question: What do I need to change to initialize the ViewModel only once?
The following is my current code (abbreviated and renamed for this question):
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels()
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
intent.getParcelableExtra<ViewModelB>("id")?.let {
Log.e(TAG, "Found parceled bData $it")
// This seems like a very stupid way to do it, is there a better one?
bData.copyAll(it)
}
}
}
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
Should I change the initialization of tagData with by viewModels() to = ViewModelB(intent)?
Or do I need to extend the ViewModelFactory somehow?
Any tip here would be really appreciated, thanks.
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
The official solution would be to provide a SavedStateHandle that is initialized with the defaultArgs as the intent.extras of your Activity.
For that, you need to provide an AbstractSavedStateViewModelFactory implementation, OR use SavedStateViewModelFactory (in which case you must define the right constructor in order to have it instantiated via reflection).
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels {
SavedStateViewModelFactory(application, this, intent.extras)
}
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
// intent.getParcelableExtra<ViewModelB>("id")?.let {
// Log.e(TAG, "Found parceled bData $it")
}
}
Then in your ViewModel
#Keep
class ViewModelB(val savedStateHandle: SavedStateHandle): ViewModel() {
val uByteData = savedStateHandle.get<UByteArray>("id")
}
Or so. The "id" key must match the same key as is in the intent extras.
Since you have a ViewModel which implements Parcelable, you can get your ViewModelB instance directly from the Intent extra.
The Intent which is used for starting ActivityB may not be != null at the time when ActivityB is instantiated, but you can use
lateinit var bData: ViewModelB
Then in onCreate()
bData = if(intent.hasExtra("id")) intent.getParcelableExtra<ViewModelB>("id") else ViewModelProvider(this).get(ViewModelB::class.java)

SharedViewModel not clearing when poping fragment

I'm using a shared view model like here
But the problem is that when I clear my last fragment, I want to clear the viewmodel, or kill its instance, but somehow it survives when I leave the last fragment that uses it
How can I programatically clear this viewmodel ?
I use it like this
Fragment A
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.getTotal().observe(viewLifecycleOwner, Observer { cartTotal ->
total = cartTotal
})
}
From fragment B I sent the total
Fragment B
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.setTotal = 10
}
But when leaving Fragment A with that data (doing popBackStack since I'm using navigation components) it does not clear the viewmodel, instead when I open again my fragment , the data stills there
I suspect that the viewmodel is tied with my Container Activity and not the lifecycle of the fragments itself, so
How can I remove the instance or clear my viewmdel when I hit my last fragment ?
Thanks
If you want to get a ViewModel associated with a parent fragment, your inner fragment should follow the by viewModels JavaDoc and use:
val viewmodel: MYViewModel by viewmodels ({requireParentFragment()})
This says to use the parent Fragment as the owner of your ViewModel.
(The parent fragment would use by viewModels() as it is accessing its own ViewModels)
you can also clear viewModelStore manually after Fragment A destroyed.
something like this :
override fun onDetach() {
super.onDetach()
requireActivity().viewModelStore.clear()
}
then your viewModel instance will be cleared. for checking this work you can debug onCleared method of your viewModel.

Shared viewModel achived fragment lifecycle

How I can use shared viewModel with fragments without activity?
Like in code but in place of requireActivity() use ParentFragment. In this case when ParentFragment will destroyed, SharedViewModel is cleared, but when I provide SharedViewModel from activity, it not cleared when ParentFragment destroyed.
And I use Navigation Components, which mean that I can`t set tag for fragment and then use findFragmentByTag()
class ParentFragment:Fragment{
override fun onCreate(savedInstanceState: Bundle?) {
var viewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class)
}
}
class ChildFragment:Fragmnet{
override fun onCreate(savedInstanceState: Bundle?) {
var viewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class)
}
}
You can try scoped-vm - it allows you to request ViewModel for scope identified by a String key. Scope lives till the last fragment that requested ViewModel gets destroyed, then ViewModel gets cleared.
You can use this code to obtain SharedViewModel both in ParentFragment and ChildFragment.
ScopedViewModelProviders
.forScope(this, "scope")
.of(requireActivity())
.get(SharedViewModel::class.java)
See you can initialize viewModel in all fragments which you want to share viewmodel, and use Rx with viewModel, your all process in these fragments will keep running until you want to cancel it,you can call viewModel.oncleard() from Activity or Fragment.
public override fun onCleared() {
super.onCleared()
compositeDisposable.dispose()
// or cancel any process
}
If you do not know ViewModel Scope, please check this image

Categories

Resources