Alternative for observeForever using Transformations.map - android

I am observing a liveData to do some queries using observeForever inside ViewModel,
query.observeForever {
//
}
the logic is working fine, but i can't remove the observer in onCleared as i have no access to lifecycle from viewModel and i shouldn't, i tried with Transformation.map
Transformations.map(query){
//
}
and failed to observe the changes.any suggestions how to use Transformation.map to listen to livedata changes and act on them

You could make your viewmodel extend LifecycleObserver like this -
class MyViewModel() : LifecycleObserver {
val queryObserver = Observer {
// do stuff
}
#OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResumed() {
query.observeForever(queryObserver)
}
#OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPasued() {
query.removeObserver(queryObserver)
}
}
Don't forget to add getLifecycle().addObserver(mViewModel) in your Activity!

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 mock Android's ProcessLifecycleOwner

I am working on a utility class that subscribes to the app going foreground/background via ProcessLifecycleOwner api. The class takes in a ProcessLifecycleOwner instance, and has the following methods which observe on lifecycle events:
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startSomething() {
appStatusSubject.onNext(AppStatus.FOREGROUNDED)
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopSomething() {
appStatusSubject.onNext(AppStatus.BACKGROUNDED)
}
I would like to write a unit test for this utility class. Is there anyway I can mock the ProcessLifecycleOwner object, have it emit certain lifecycle events, and assert that appStatusSubject.onNext(...) is called?
Would really appreciate any advice.
There is a TestLifecycleOwner, which probably does what you want.
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:lifecycle/lifecycle-runtime-testing/src/main/java/androidx/lifecycle/testing/TestLifecycleOwner.kt;l=38
It's part of the androidx.lifecycle:lifecycle-runtime-testing artifact.
If you want to create a super simple fake for the LifecycleOwner, you can do so as follows:
class TestLifecycleOwner() : LifecycleOwner {
private val registry = LifecycleRegistry(this).apply {
currentState = Lifecycle.State.RESUMED
}
override fun getLifecycle(): Lifecycle = registry
}
Then you can simply call:
testLifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)

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

Android LiveData Observer not active after first update

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.

Categories

Resources