Lifecycle scoping in a Fragment - android

I'm having a hard time understand what scopes to use for view models and live data when using fragments. Here is my ViewModel:
class MyViewModel: ViewModel() {
var myLiveData = MutableLiveData<WrappedResult<DataResponse>>()
private val repository = MyRespository()
private var job: Job? = null
fun getData(symbol: String) {
job = viewModelScope.launch(Dispatchers.IO) {
try {
val response = repository.getData(symbol)
withContext(Dispatchers.Main) {
myLiveData.value = WrappedResult.Success(response)
}
} catch(e: Exception) {
withContext(Dispatchers.Main) {
myLiveData.value = WrappedResult.Failure(e)
}
}
}
}
}
I can create the view model in the fragment using (where "this" is the fragment):
viewModel = new ViewModelProvider(this).get(MyViewModel.class);
However, I can observe the LiveData with two options:
viewModel.getMyLiveData.observe(this...
or
viewModel.getMyLiveData.observe(getViewLifecycleOwner()...
It would appear that the job I create in the view model is going to be scoped to the fragment's lifecycle (through viewModelScope) and not the fragment's view lifecycle, but I have a choice between these two for the live data.
I could use some guidance and what the best practice is here. Also, does any of this matter whether the fragment has retained instance or not? Currently the fragment has setRetainInstance(true). Finally, from everything I've read I shouldn't need to clear the observer in the fragment or override onCleared when things are setup this way. Is that correct?

refer the doc of view model
https://developer.android.com/topic/libraries/architecture/viewmodel?gclid=Cj0KCQjwtZH7BRDzARIsAGjbK2blIS5rGzBxBdX6HpB5PMKgpUQHvdKXbwrt-ukTnWkpax1otMk4sm4aAuzPEALw_wcB&gclsrc=aw.ds#lifecycle
Viewmodel will only gets destoyed once the activity is finished.As the fragments are on the top of acitivity, the lifecycle of fragment will not affect the Viewmodel.The data will be persisted there on the viewmodel. So you can write a method to reset the data in viewmodel while you are entering in to oncreate of fragment.
In Fragment, OnCreate :
getViewModel.init()
on ViewModel
fun init() {
// clear all varialbes/datas/ etc here
}

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

ViewModel and LiveData Observer Not Invoking

Observer not calling in Second Fragment. Any help would be greatly appreciated.
Fragment1
viewModel.productData(model)
findNavController().navigate(R.id.actionProductListToDetails)
ViewModel class
val productData = MutableLiveData<ModelProductSubItem>()
fun productData(data: ModelProductSubItem) {
productData.value = data
}
Fragment2
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(ProductSubListItemViewModel::class.java)
viewModel.productData.observe(viewLifecycleOwner, Observer{ it ->
println("Product_Name"+it.product_name) // Not invoking
})
Tried with
viewModel = activity?.run {
ViewModelProvider(this).get(ProductSubListItemViewModel::class.java)
} ?: throw Exception("Invalid Activity")
})
FYI
I have same ViewModelProvider
I think the problem is there may be two ViewModelProvider(this), each using "this" on a different fragment. If you have two providers, then there are two separate models that do not share their triggers.
That's why, when you call ViewModelProvider you call the activity which holds the fragments as the single source:
viewmodel = activity?.run {
ViewModelProviders.of(this).get(SomeViewModel::class.java)
} ?: throw Exception("Invalid Activity")
Although with the newer version of ViewModel and fragment-ktx artifact this is not neccesary, I would recommend using this latest version.
Most likely what's happening is that, despite your FYI comment, you are actually instantiating different ViewModels for each context. If you want to share the same instance of an Activity ViewModel among multiple Fragments, then each fragment should get the shared ViewModel like this:
new ViewModelProvider(requireActivity()).get(SomeViewModel.class)
Instead of
new ViewModelProvider(this)).get(SomeViewModel.class)
If you are using Kotlin extensions (KTX) I believe you should do this:
val viewModel by activityViewModels<SomeViewModel>()
Instead of
val viewModel by viewModels<SomeViewModel>()

Android ViewModelProvider() parameter error

I am trying to get a value from the SharedViewModel class but the ViewModelProvider() is giving a parameter error when i am passing requireActivity() although the same initilization and assignment works in my fragments.
It is requiring "ViewModelStoreOwner" to be passed.
class CourseRepository(val app: Application) {
private var viewModel: SharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
if (Utility.networkAvailable(app)) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(viewModel.pathName).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
}
The purpose of the ViewModel here is because i am storing the Id of the selected RecyclerView item in order to send it to a server
ViewModel instances are scoped to Fragments or Activities (or anything with a similar lifecycle), which is why you need to pass in a ViewModelStoreOwner to the provider to get a ViewModel from it. The point of ViewModels is that they will exist until the store they belong to is destroyed.
The requireActivity method doesn't work here, because you're not inside a Fragment.
Some things to consider here:
Do you really need ViewModel in this use case? Could you perhaps use just a regular class that you can create by calling its constructor?
Could you call this Repository from your ViewModel, and pass in any parameters you need from there?

How to use Dagger 2 to Inject ViewModel of same Fragments inside ViewPager

I am trying to add Dagger 2 to my project. I was able to inject ViewModels (AndroidX Architecture component) for my fragments.
I have a ViewPager which has 2 instances of the same fragment (Only a minor change for each tabs) and in each tab, I am observing a LiveData to get updated on data change (from API).
The issue is that when the api response comes and updates the LiveData, the same data in the currently visible fragment is being sent to observers in all the tabs. (I think this is probably because of the scope of the ViewModel).
This is how I am observing my data:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activityViewModel.expenseList.observe(this, Observer {
swipeToRefreshLayout.isRefreshing = false
viewAdapter.setData(it)
})
....
}
I am using this class for providing ViewModels:
class ViewModelProviderFactory #Inject constructor(creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>?) :
ViewModelProvider.Factory {
private val creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>? = creators
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel?>? = creators!![modelClass]
if (creator == null) { // if the viewmodel has not been created
// loop through the allowable keys (aka allowed classes with the #ViewModelKey)
for (entry in creators.entries) { // if it's allowed, set the Provider<ViewModel>
if (modelClass.isAssignableFrom(entry.key!!)) {
creator = entry.value
break
}
}
}
// if this is not one of the allowed keys, throw exception
requireNotNull(creator) { "unknown model class $modelClass" }
// return the Provider
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
companion object {
private val TAG: String? = "ViewModelProviderFactor"
}
}
I am binding my ViewModel like this:
#Module
abstract class ActivityViewModelModule {
#MainScope
#Binds
#IntoMap
#ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}
I am using #ContributesAndroidInjector for my fragment like this:
#Module
abstract class MainFragmentBuildersModule {
#ContributesAndroidInjector
abstract fun contributeActivityFragment(): ActivityFragment
}
And I am adding these modules to my MainActivity subcomponent like this:
#Module
abstract class ActivityBuilderModule {
...
#ContributesAndroidInjector(
modules = [MainViewModelModule::class, ActivityViewModelModule::class,
AuthModule::class, MainFragmentBuildersModule::class]
)
abstract fun contributeMainActivity(): MainActivity
}
Here is my AppComponent:
#Singleton
#Component(
modules =
[AndroidSupportInjectionModule::class,
ActivityBuilderModule::class,
ViewModelFactoryModule::class,
AppModule::class]
)
interface AppComponent : AndroidInjector<SpenmoApplication> {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
}
I am extending DaggerFragment and injecting ViewModelProviderFactory like this:
#Inject
lateinit var viewModelFactory: ViewModelProviderFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
....
activityViewModel =
ViewModelProviders.of(this, viewModelFactory).get(key, ActivityViewModel::class.java)
activityViewModel.restartFetch(hasReceipt)
}
the key will be different for both the fragments.
How can I make sure that only the observer of the current fragment is getting updated.
EDIT 1 ->
I have added a sample project with the error. Seems like the issue is happening only when a custom scope is added. Please check out the sample project here: Github link
master branch has the app with the issue. If you refresh any tab (swipe to refresh) the updated value is getting reflected in both the tabs. This is only happening when I add a custom scope to it (#MainScope).
working_fine branch has the same app with no custom scope and its working fine.
Please let me know if the question is not clear.
I want to recap the original question, here's it:
I am currently using the working fine_branch, but I want to know, why would using scope break this.
As per my understanding your have an impression, that just because you are trying to obtain an instance of ViewModel using different keys, then you should be provided different instances of ViewModel:
// in first fragment
ViewModelProvider(...).get("true", PagerItemViewModel::class.java)
// in second fragment
ViewModelProvider(...).get("false", PagerItemViewModel::class.java)
The reality, is a bit different. If you put following log in fragment you'll see that those two fragments are using the exact same instance of PagerItemViewModel:
Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")
Let's dive in and understand why this happens.
Internally ViewModelProvider#get() will try to obtain an instance of PagerItemViewModel from a ViewModelStore which is basically a map of String to ViewModel.
When FirstFragment asks for an instance of PagerItemViewModel the map is empty, hence mFactory.create(modelClass) is executed, which ends up in ViewModelProviderFactory. creator.get() ends up calling DoubleCheck with following code:
public T get() {
Object result = instance;
if (result == UNINITIALIZED) { // 1
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
instance = reentrantCheck(instance, result); // 2
/* Null out the reference to the provider. We are never going to need it again, so we
* can make it eligible for GC. */
provider = null;
}
}
}
return (T) result;
}
The instance is now null, hence a new instance of PagerItemViewModel is created and is saved in instance (see // 2).
Now the exact same procedure happens for SecondFragment:
fragment asks for an instance of PagerItemViewModel
map now is not empty, but does not contain an instance of PagerItemViewModel with key false
a new instance of PagerItemViewModel is initiated to be created via mFactory.create(modelClass)
Inside ViewModelProviderFactory execution reaches creator.get() whose implementation is DoubleCheck
Now, the key moment. This DoubleCheck is the same instance of DoubleCheck that was used for creating ViewModel instance when FirstFragment asked for it. Why is it the same instance? Because you've applied a scope to the provider method.
The if (result == UNINITIALIZED) (// 1) is evaluating to false and the exact same instance of ViewModel is being returned to the caller - SecondFragment.
Now, both fragments are using the same instance of ViewModel hence it is perfectly fine that they are displaying the same data.
Both the fragments receive the update from livedata because viewpager keeps both the fragments in resumed state.
Since you require the update only on the current fragment visible in the viewpager, the context of the current fragment is defined by the host activity, the activity should explicitly direct updates to the desired fragment.
You need to maintain a map of Fragment to LiveData containing entries for all the fragments(make sure to have an identifier that can differentiate two fragment instances of the same fragment) added to viewpager.
Now the activity will have a MediatorLiveData observing the original livedata observed by the fragments directly. Whenever the original livedata posts an update, it will be delivered to mediatorLivedata and the mediatorlivedata in turen will only post the value to livedata of the current selected fragment. This livedata will be retrieved from the map above.
Code impl would look like -
class Activity {
val mapOfFragmentToLiveData<FragmentId, MutableLiveData> = mutableMapOf<>()
val mediatorLiveData : MediatorLiveData<OriginalData> = object : MediatorLiveData() {
override fun onChanged(newData : OriginalData) {
// here get the livedata observed by the currently selected fragment
val currentSelectedFragmentLiveData = mapOfFragmentToLiveData.get(viewpager.getSelectedItem())
// now post the update on this livedata
currentSelectedFragmentLiveData.value = newData
}
}
fun getOriginalLiveData(fragment : YourFragment) : LiveData<OriginalData> {
return mapOfFragmentToLiveData.get(fragment) ?: MutableLiveData<OriginalData>().run {
mapOfFragmentToLiveData.put(fragment, this)
}
}
class YourFragment {
override fun onActivityCreated(bundle : Bundle){
//get activity and request a livedata
getActivity().getOriginalLiveData(this).observe(this, Observer { _newData ->
// observe here
})
}
}

Use ViewModelFactory inside Fragment

I'm trying to share a ViewModel between my activity and my fragment. My ViewModel contains a report, which is a complex object I cannot serialize.
protected val viewModel: ReportViewModel by lazy {
val report = ...
ViewModelProviders.of(this, ReportViewModelFactory(report)).get(ReportViewModel::class.java)
}
Now I'm trying to access the viewmodel in a fragment, but I don't want to pass all the factory parameters again.
As stated by the ViewModelProvider.get documentation:
Returns an existing ViewModel or creates a new one in the scope
I want to access the ViewModel instance defined in the activity, so I tried the following but it logically crashes as the model doesn't have an empty constructor:
protected val viewModel: ReportViewModel by lazy {
ViewModelProviders.of(requireActivity()).get(ReportViewModel::class.java)
}
How one should access its "factorysed" ViewModels in a fragment? Should we pass the factory to the fragment?
Thanks!
A little late but I had this question myself. What I found is you can do the following:
In your activity override getDefaultViewModelProviderFactory() like so:
override fun getDefaultViewModelProviderFactory(): ReportViewModelFactory {
return ReportViewModelFactory(report)
}
now in your fragments you can do
requireActivity().getDefaultViewModelProviderFactory()
to get the factory.
Or simply instantiate your viewModel like:
private val viewModel: ReportViewModel by activityViewModels()

Categories

Resources