android-implement ViewModel between 2 fragments without using getActivity() - android

I have 2 fragments in my app. when the user clicks on a button in the first fragment, the second fragment gets added so that the user can insert some data. and then it gets closed and gives the inserted data back to the first fragment. I've used ViewModels for this communication between fragments.
collectionsEditedViewModel = new ViewModelProvider(getActivity()).get(CollectionsEditedViewModel.class);
collectionsEditedViewModel.isEdited().observe(getViewLifecycleOwner(), new Observer<Bundle>() {
#Override
public void onChanged(Bundle bundle) {
}
});
the communication is working properly. but the point is that how can I define the scope of this communication within the fragments. currently I'm using getActivity() as ViewmodelStoreOwner which causes the set data to be redelivered to the first fragment whenever it is reopened. how can I solve this issue?

I believe for communication between Fragments, via the Activity is the way to go, so you're in the correct path.
One thing you could do is use a SingleLiveData class, which essentially is like LiveData but after its value is set, it's nullified, so only the first observer gets it:
class SingleLiveData<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveData"
}
}
Then you can simply call:
singleLiveData.call() for a "set and destroy" and thus, forget your value after the first use! Class retrieved (and been using in my projects for years) from: https://stackoverflow.com/a/46862551/

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.

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

Why is ViewModelScoped coroutine unusable after ViewModel onCleared() method called

I am sharing an ActivityScoped viewModel between multiple Fragments in my current Android application.
The viewModel employs Coroutine Scope viewModelScope.launch{}
My issue is the .launch{} only works until the owning ViewModel onCleared() method is called.
Is this how ViewModel scoped coroutines are supposed to work?
Is there an approach I can use to "Reset" the viewModelScope so that .launch{} works following the onCleared() method being called?
heres my code::
Fragment
RxSearchView.queryTextChangeEvents(search)
.doOnSubscribe {
compositeDisposable.add(it)
}
.throttleLast(300, TimeUnit.MILLISECONDS)
.debounce(300, TimeUnit.MILLISECONDS)
.map { event -> event.queryText().toString() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { charactersResponse ->
launch {
viewModel.search(charactersResponse.trim())
}
}
.
.
.
override fun onDetach() {
super.onDetach()
viewModel.cancelSearch()
compositeDisposable.clear()
}
ViewModel
suspend fun search(searchString: String) {
cancelSearch()
if (TextUtils.isEmpty(searchString)) {
return
}
job = viewModelScope.launch {
repository.search(searchString)
}
}
fun cancelSearch() {
job?.cancelChildren()
}
.
.
.
override fun onCleared() {
super.onCleared()
repository.onCleared()
}
What am I doing wrong?
UPDATE
If I amend my launch code to this
job = GlobalScope.launch {
repository.search(searchString)
}
It solves my issue, however is this the only way to achieve my desired result?
I was under the impression GlobalScope was "Bad"
following a cal to onCleared() my viewModelScoped cororoutine Launch stops executing
That's a feature, not a bug.
Once the ViewModel is cleared, you should not be doing anything in that ViewModel or whatever its LifecycleOwner was. All of that is now defunct and should no longer be used.
however is this the only way to achieve my desired result?
The correct solution is to get rid of the code from the ViewModel. If you are expecting some background work to go past the lifetime of an activity or fragment, then that code does not belong in the activity/fragment or its associated viewmodels. It belongs in something that has a matching lifetime to the work that you are trying to do.
repository.onCleared()
This method should not belong to the Repository.
In fact, the Repository should not be stateful.
If you check Google's samples, the Repository creates a LiveData that contains a Resource, and the reason why this is relevant is because the actual data loading and caching mechanic is inside this resource, triggered by LiveData.onActive (in this sample, MediatorLiveData.addSource, but technically that's semantically the same thing).
.subscribe { charactersResponse ->
launch {
viewModel.search(charactersResponse.trim())
The Fragment shouldn't be launching coroutines. It should say something like
.subscribe {
viewModel.updateSearchText(charactersResponse.trim())
}
and also
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java, factory)
viewModel.searchResults.observe(viewLifecycleOwner, Observer { results ->
searchAdapter.submitList(results)
})
}
Then ViewModel would
class MyViewModel(
private val repository: MyRepository
): ViewModel() {
private val searchText = MutableLiveData<String>()
fun updateSearchText(searchText: String) {
this.searchText.value = searchText
}
val searchResults: LiveData<List<MyData>> = Transformations.switchMap(searchText) {
repository.search(searchText)
}
}
And that's all there should be in the ViewModel, so then the question of "who owns the coroutine scope"? That depends on when the task should be cancelled.
If "no longer observing" should cancel the task, then it should be LiveData.onInactive() to cancel the task.
If "no longer observing but not cleared" should retain the task, then ViewModel's onCleared should indeed govern a SupervisorJob inside the ViewModel that would be cancelled in onCleared(), and the search should be launched within that scope, which is probably only possible if you pass over the CoroutineScope to the search method.
suspend fun search(scope: CoroutineScope, searchText: String): LiveData<List<T>> =
scope.launch {
withContext(Dispatchers.IO) { // or network or something
val results = networkApi.fetchResults(searchText)
withContext(Dispatchers.MAIN) {
MutableLiveData<List<MyData>>().apply { // WARNING: this should probably be replaced with switchMap over the searchText
this.value = results
}
}
}
}
Would this work? Not sure, I don't actually use coroutines, but I think it should. This example however doesn't handle the equivalent of switchMap-ing inside the LiveData, nor with coroutines.

How to observe ViewModel that makes a request for a call back

I am trying to make a request to a library that gives me a call back.
Manager.getInstance().request(new CallBack())
I want to put this in a ViewModel so that I can observe it from the Activity.
class RequestViewModel : ViewModel, CallBack {
fun request() {
Manager.getInstance().request(this)
}
override fun onFinished(result : List<String>?) {
}
override fun onFailed() {
}
}
How can I make it so that I can observe when this has finished? I know I could make my Activity implement this CallBack, but I don't want to couple Activity to this.
Ideally this would be a LiveData or Observable.
If I understand the question correctly, you can submit the data acquired in onFinished method to the LiveData instance that should be observed by a view component, e.g.
class RequestViewModel : ViewModel, CallBack {
private val _liveData = MutableLiveData<SomeResult<List<String>>>
val liveData: LiveData<SomeResult<List<String>>> get() = _liveData
fun request() {
Manager.getInstance().request(this)
}
override fun onFinished(result : List<String>?) {
if (result != null) {
_liveData.postValue(SomeResult.success(result))
} else {
_liveData.postValue(SomeResult.failure())
}
}
override fun onFailed() {
_liveData.postValue(SomeResult.failure())
}
}
And somewhere in your object that corresponds to a view component:
viewModel.liveData.observe(lifecycleOwner, Observer<List<String>> {
handleResponse(it)
})
whereas lifecycleOwner typically is your AppCompatActivity or android.support.v4.Fragment inheritor.
I would advise you to decouple requesting from ViewModel and create a class called Repository to handle all the requests. In this class you could have a MutableLiveData object which can be observed and whenever new requested data is retrieved, use mutableLiveData.postValue(retrievedData) for MutableLiveData which notifies the observes about the new changes.
To read more about repository, you can follow these links:
Google's Guide to App Architecture
Codelab tutorial with Repository pattern

Should I include LifecycleOwner in ViewModel?

LifecycleOwner is currently needed in order for me to create an observer.
I have code which creates an Observer in the ViewModel so I attach the LifecycleOwner when retrieving the ViewModel in my Fragment.
According to Google's documentation.
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
Did I break that warning and If I did, what way do you recommend me to move my creation of an observer for data return?
I only made an observer so I'm wondering if it's still valid. Since also in Google's documentation it also said.
ViewModel objects can contain LifecycleObservers, such as LiveData objects.
MainFragment
private lateinit var model: MainViewModel
/**
* Observer for our ViewModel IpAddress LiveData value.
* #see Observer.onChanged
* */
private val ipObserver = Observer<String> {
textIp.text = it
hideProgressBar()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = ViewModelProviders.of(this).get(MainViewModel::class.java)
model.attach(this)
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater?.inflate(R.layout.fragment_main, container, false)
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
buttonRetrieveIp.setOnClickListener {
showProgressBar()
model.fetchMyIp().observe(this, ipObserver) //Here we attach our ipObserver
}
}
override fun showProgressBar() {
textIp.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
override fun hideProgressBar() {
progressBar.visibility = View.GONE
textIp.visibility = View.VISIBLE
}
MainViewModel
private var ipAddress = MutableLiveData<String>()
private lateinit var owner: LifecycleOwner
fun attach(fragment: MainFragment) {
owner = fragment
}
/**
* For more information regarding Fuel Request using Fuel Routing and Live Data Response.
* #see Fuel Routing Support
* #see Fuel LiveData Support
* */
fun fetchMyIp(): LiveData<String> {
Fuel.request(IpAddressApi.MyIp())
.liveDataResponse()
.observe(owner, Observer {
if (it?.first?.statusCode == 200) {//If you want you can add a status code checker here.
it.second.success {
ipAddress.value = Ip.toIp(String(it))?.ip
}
}
})
return ipAddress
}
Update 1: Improved ViewModel thanks to #pskink suggestion for using Transformations.
private lateinit var ipAddress:LiveData<String>
/**
* Improved ViewModel since January 23, 2018 credits to pskink <a href="
*
* For more information regarding Fuel Request using Fuel Routing and Live Data Response.
* #see Fuel Routing Support
* #see Fuel LiveData Support
* */
fun fetchMyIp(): LiveData<String> {
ipAddress = Transformations.map(Fuel.request(IpAddressApi.MyIp()).liveDataResponse(), {
var ip:String? = ""
it.second.success {
ip = Ip.toIp(String(it))?.ip
}
ip
})
return ipAddress
}
No. If you wish to observe changes of some LiveData inside your ViewModel you can use observeForever() which doesn't require LifecycleOwner.
Remember to remove this observer on ViewModel's onCleared() event:
val observer = new Observer() {
override public void onChanged(Integer integer) {
//Do something with "integer"
}
}
...
liveData.observeForever(observer);
...
override fun onCleared() {
liveData.removeObserver(observer)
super.onCleared()
}
Very good reference with examples of observe LiveData.
Assumptions:
Fuel refers to your ViewModel
Fuel.request(IpAddressApi.MyIp()) is a method in your ViewModel
IpAddressApi.MyIp() does not have a reference to your LifecycleOwner,
If all are true,then you are not violating it. So long as you are not passing a LifecycleOwner reference to the ViewModel you are safe!
LifecycleOwner - relates to an Activity or Fragment as it owns the various Android Lifecycles e.g onCreate, onPause, onDestroy etc
in Kotlin this can be something like:
val mObserver = Observer<List<QueueTabData>> { myString->
// do something with myString
}
Should I include LifecycleOwner in ViewModel?
Ans: No
The purpose of viewmodel is to hold UI data, so that it survives across configuration changes.
And the reason for the following
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
Is because the viewmodel survives configuration changes whereas activities don't. They are destroyed and re-created on configuration change. So, if you have any activity context references in viewmodel they would refer to previous activity that got destroyed.
So this leads to memory leak. And hence it is not recommended.
Furthermore,
If you have repositories that act as your data source we should avoid using LiveData for such purposes as mentioned here in the paragraph just above the code block.
This is because LiveData are handled on MainThread that may lead to UI freeze.
We should use kotlin flows for such purposes.

Categories

Resources