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.
Related
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
}
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/
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
I'm trying out a basic implementation of Architecture Component's Live Data with Kotlin like this:
class MarketFragment : LifecycleFragment(){
......
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel=ViewModelProviders.of(this).get(MarketViewModel::class.java)
viewModel.book?.observe(this, Observer { book-> //updateUI })
....
My ViewModel class is created like this:
class MarketViewModel : ViewModel()
{
var book: MutableLiveData<Book>? =MutableLiveData()
var repository: Repository?= Repository()
init {
update("parameter")
}
fun update(s: String)
{
book=repository?.getBook(s)
}
}
And My Repository:
fun getBook(bookSymbol:String):MutableLiveData<Book>
{
val book=MutableLiveData<Book>()
......
call . enqueue (object : Callback<Book> {
override fun onResponse(call: Call<Book>?, response: Response<Book>?) {
book.value=response?.body()
}
.....
})
return book
}
}
And all of this works great and UI is updated as it should but only for the first time. If i try to make manual calls to update the viewModel from a UI action, the retrofit call still works as expected but the new data is not sent to the Observer in the Fragment:
//this doesn't work:
viewModel.update("string")
//This returns false:
viewModel.book.hasActiveObservers()
Is it the expected behaviour for an Observer to become inactive after the first trigger?
You are creating a new MutableLiveData instance each time you are calling getBooks
Hence your observer is not observing the right LiveData anymore.
To solve this
Your ViewModel should keep only one (immutable) LiveData instance
That immutable LiveData instance could either be:
A MediatorLiveData, which source is the repository's LiveData
A transformation of the repository's LiveData
That implies the repository method getBooks is only called once on initialization of the ViewModel, and that either refreshes itself OR you have to call another method on the repository to trigger a refresh.
Trying to figure out the Google's recent tools and concepts: LifecycleActivity, ViewModel and Data Binding.
So imagine there is a FooActivity that extends AppCompatActivity(to be able to use Support library) and implements LifecycleOwner interface (from reference: required to use LiveData):
FooActivity.kt:
class FooActivity: AppCompatActivity(), LifecycleObserver {
...
We set the binding:
..
private val mBinding: by lazy {
DataBindingUtil.setContentView<ActivityFooBinding>(this, R.layout.activity_foo)
}
..
We set activity's ViewModel, and attach our barObserver (which should observe changes of bar inside a ViewModel, which is just a List of Strings):
..
val viewModel = ViewModelProviders.of(this).get(FooViewModel::class.java)
viewModel.getBars()?.observe(this, barObserver)
viewModel.init() // changes bar, should trigger Observer's onChanged
..
And finally we define barObserver:
..
class BarObserver : Observer<List<String>> {
override fun onChanged(p0: List<String>?) {
Log.d("Say", "changed:")
}
}
barObserver = BarObserver()
..
Questions:
Why does Observers onChange never triggered?
Should one use LifeCycleOwner``getLifecycle instead of using Observer?
Any other thoughts?
Edit:
As the reference states:
LiveData is a data holder class that can be observed within a given lifecycle. This means that an Observer can be added in a pair with a LifecycleOwner, and this observer will be notified about modifications of the wrapped data only if the paired LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is STARTED or RESUMED. An observer added via observeForever(Observer) is considered as always active and thus will be always notified about modifications. For those observers, you should manually call removeObserver(Observer).
Changing:
enter code hereviewModel.getBars()?.observe(this, barObserver)
to:
viewModel.getBars()?.observeForever(barObserver)
didn't to the trick. onChange still never triggered.
Edit2:
FooActivity.kt:
replaced with:
class FooActivity: AppCompatActivity(), LifecycleRegistryOwner{
override fun getLifecycle(): LifecycleRegistry {
var lifecycle = LifecycleRegistry(this)
lifecycle.addObserver(barObserver)
return lifecycle
}
FooViewModel:
..
private var mBar = MutableLiveData<List<String>>()
..
mBar.value = listValueOf("1", "2", "3")
..
fun getBar(): LiveData<List<String>>? = mBar
..
If you are not extending LifecycleActivity then your activity should implement LifecycleRegistryOwner instead of LifecycleObserver. Check out the docs.
And then you can pass it to liveData.observe(lifecycleOwner, observer)