So, Im quite new in Android development and also with working on observables. So what I want to achieve is that on every scan tick of the bluetooth scanner, the scanResult gets checked and if its not in the list, the observed list scanned gets an update, which checks something if this is valid and makes something.
My Problem is that the observe function just runs through once and then never again
2021-03-05 22:18:54.522 32057-32057/? D/devices: Got something []
2021-03-05 22:18:54.522 32057-32057/? D/devices: Start scan
2021-03-05 22:18:55.209 32057-32057/? D/devices: Checking Had128_1_1 in scanned
2021-03-05 22:18:55.212 32057-32057/? D/devices: adding Had128_1_1 in scanned
As far as I understood it right how this should work, he should here _scanned.value?.add(scanResult.device) update and the observer should recognize that or am I wrong?
Could id be a problem with the lifecycleOwner?
It comes from the MainActivity to the composable with val lifecycleOwner = LocalLifecycleOwner.current
And yes, the function gets called in my composable (as I said, if i press the button, I can see on "Got Something []" that it runs through the first time)
private val _scanned: MutableLiveData<MutableList<BluetoothDevice>> =
MutableLiveData(mutableListOf())
private val scanned: LiveData<MutableList<BluetoothDevice>> get() = _scanned
private val lockScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
result?.let { scanResult ->
if (fakeWhitelist.contains(scanResult.device.name)) {
Log.d("devices", "Checking ${scanResult.device.name} in scanned")
val search =
_scanned.value?.find { device -> device.name == scanResult.device.name }
if (search == null) {
Log.d("devices", "adding ${scanResult.device.name} in scanned")
_scanned.value?.add(scanResult.device)
}
}
}
}
}
fun listenForDevices(viewLiveCycleOwner: LifecycleOwner) {
scanned.observe(viewLiveCycleOwner, Observer { deviceList ->
Log.d("devices", "Got something $deviceList")
locationList.value.forEach { locationScaffold ->
val boxNames = locationScaffold.parcelBox.map { it.boxName }
val device = deviceList.last()
Log.d("devices", "$boxNames")
if (!boxNames.contains(device.name)) {
Log.d("devices", "Getting location for ${device.name}")
getLocation(device.name)
}
}
})
startScan()
}
Your mistake here is that you are never changing the value of the LiveData. The value of LiveData can be retrieved using LiveData.getValue() and can only be set using LiveData.setValue() and if you have a look over your implementation, you don't use the set method anywhere.
It is quite a common misconception that changing the state of an object will do something to it's reference, or somehow notify a class holding a reference to it, but this is not the case. If you think about a simplified version of LiveData you can break it down into having these core functions:
Having a current value
Holding a list of potential observers
Notifying said observers when the value is changed
The only way for it to be able to notify the observers is to expose a method/function that not only sets the value, but also notifies all the observers of the change if necessary.
So in your implementation, you retrieved the current value and changed its state, but never actually set a new value to the LiveData.
So instead of
liveData.value?.add(newItem)
We would need to do something like
val list = liveData.value
list?.add(newItem)
_mutableLiveData.value = list
Related
Is this good to put the collect latest inside observe?
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
.observe(viewLifecycleOwner) {
if (it != null) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.referralDetailsResponse.collect { referralResponseState ->
when (referralResponseState) {
State.Empty -> {
}
is State.Failed -> {
Timber.e("${referralResponseState.message}")
}
State.Loading -> {
Timber.i("LOADING")
}
is State.Success<*> -> {
// ACCESS LIVEDATA RESULT HERE??
}}}}
I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?
My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response
private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()
init {
val inviteSlug: String? = savedStateHandle["inviteSlug"]
// Fire invite link
if (inviteSlug != null) {
referralDetail(inviteSlug)
}
}
fun referralDetail(referral: String?) = viewModelScope.launch {
_referralDetailsResponse.send(State.Loading)
when (
val response =
groupsRepositoryImpl.referralDetails(referral)
) {
is ResultWrapper.GenericError -> {
_referralDetailsResponse.send(State.Failed(response.error?.error))
}
ResultWrapper.NetworkError -> {
_referralDetailsResponse.send(State.Failed("Network Error"))
}
is ResultWrapper.Success<*> -> {
_referralDetailsResponse.send(State.Success(response.value))
}
}
}
fun fetchUserProfileLocal(username: String) =
userRepository.getUserLocal(username).asLiveData()
You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:
combine(
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
viewModel.referralDetailsResponse
) { userProfile, referralResponseState ->
...
}.launchIn(viewLifecycleOwner.lifecycleScope)
But it is better to move combining logic to ViewModel class and observe the overall result.
Dependency to use LiveData.asFlow() extension function:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
it certainly is not a good practice to put a collect inside the observe.
I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata
for example in your first code block I would change it like this
get rid of "userProfile" from your viewmodel
create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
then in your viewmodel, where you would update the "userProfile" update the three new state objects instead
this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel
For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:
private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()
What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again
I have a DAO class where I have fetchHubList method which fetches a collection of documents from cloud Firestore asynchronously using await(). This implementation used the "get()" method which I got to know later on does not fetch real-time updates. On trying to implement the code similarly using onSnapshotListener gives an error (which was quite expected to be honest, because get() and this methods return quite different things). Does anyone have any idea how to implement this?
How the code is currently:
suspend fun fetchHubList(): ArrayList<HubModel>? = try {
val hubList = ArrayList<HubModel>()
hubsListCollection.get().await().map { document ->
if (document != null) {
Log.d(TAG, "Data fetch successful!")
Log.d(TAG, "the document id is ${document.id}")
val temp = HubModel(document.get("hubName").toString(),
document.id.toString(),
document.get("isAdmin") as Boolean)
hubList.add(temp)
// hubList.add(document.toObject(HubModel::class.java))
} else {
Log.d(TAG, "No such document")
}
}
And what I want to implement here (and which is totally erroneous):
suspend fun fetchHubList(): ArrayList<HubModel>? = try {
val hubList = ArrayList<HubModel>()
hubsListCollection.addSnapshotListener().await().map { document ->
if (document != null) {
Log.d(TAG, "Data fetch successful!")
Log.d(TAG, "the document id is ${document.id}")
val temp = HubModel(document.get("hubName").toString(),
document.id.toString(),
document.get("isAdmin") as Boolean)
hubList.add(temp)
// hubList.add(document.toObject(HubModel::class.java))
} else {
Log.d(TAG, "No such document")
}
}
I use this function in my ViewModel class to create a LiveData wrapped ArrayList:
val hubList = MutableLiveData<ArrayList<HubModel>>()
private val hubListDao = HubListDao()
init {
viewModelScope.launch {
hubList.value = hubListDao.fetchHubList()
}
}
Thanks in advance!
You don't need addSnapshotListener, just use get:
hubsListCollection.get().await()
In order to observe changes in your collection you can extend LiveData:
class CafeLiveData(
private val documentReference: DocumentReference
) : LiveData<Cafe>(), EventListener<DocumentSnapshot> {
private var snapshotListener: ListenerRegistration? = null
override fun onActive() {
super.onActive()
snapshotListener = documentReference.addSnapshotListener(this)
}
override fun onInactive() {
super.onInactive()
snapshotListener?.remove()
}
override fun onEvent(result: DocumentSnapshot?, error: FirebaseFirestoreException?) {
val item = result?.let { document ->
document.toObject(Cafe::class.java)
}
value = item!!
}
}
And expose it from your view model:
fun getCafe(id: String): LiveData<Cafe> {
val query = Firebase.firestore.document("cafe/$id")
return CafeLiveData(query)
}
As #FrankvanPuffelen already mentioned in his comment, there is no way you can use ".await()" along with "addSnapshotListener()", as both are two totally different concepts. One is used to get data only once, while the second one is used to listen to real-time updates. This means that you can receive a continuous flow of data from the reference you are listening to.
Please notice that ".await()" is used in Kotlin with suspend functions. This means that when you call ".await()", you start a separate coroutine, which is a different thread that can work in parallel with other coroutines if needed. This is called async programming because ".await()" starts the coroutine execution and waits for its finish. In other words, you can use ".await()" on a deferred value to get its eventual result, if no Exception is thrown. Unfortunately, this mechanism doesn't work with real-time updates.
When it comes to Firestore, you can call ".await()" on a DocumentReference object, on a Query object, or on a CollectionReference object, which is actually a Query without filters. This means that you are waiting for the result/results to be available. So you can get a document or multiple documents from such calls. However, the following call:
hubsListCollection.addSnapshotListener().await()
Won't work, as "addSnapshotListener()" method returns a ListenerRegistration object.
I want to use a snapshot listener to listen to changes that might occur in my database to update my RecyclerView
In this case, you should consider using a library called Firebase-UI for Android. In this case, all the heavy work will be done behind the scenes. So there is no need for any coroutine or ".await()" calls, everything is synched in real-time.
If you don't want to use either Kotlin Coroutines, nor Firebase-UI Library, you can use LiveData. A concrete example can be seen in my following repo:
https://github.com/alexmamo/FirestoreRealtimePagination/blob/master/app/src/main/java/ro/alexmamo/firestorerealtimepagination/ProductListLiveData.java
Where you can subclass LiveData class and implement EventListener the interface.
I have a class which monitors the network state.
I'm using Flow to be able to collect the network state updates.
However, I also need to leave an option to use manual listeners that programmers can "hook" onto to be able to receive the network changes.
My code is simple :
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> by notifyDelegate(networkTypeState)
private fun <T> notifyDelegate(init: T) =
Delegates.observable(init) { prop, _, new ->
Lg.i("notify subscribers of network update: ${prop.name} = $new")
notifySubscribers()
}
sealed class NetworkState {
object Unknown: NetworkState()
object Disconnected: NetworkState()
data class Connected(val isCellularOn: Boolean, val isWifiOn: Boolean): NetworkState()
}
Then when I update the state,
for example :
networkTypeState.value = NetworkState.Disconnected ,
the delegates.observable does not get called.
Worth noting, when I use networkTypeAsFlow.collect { .. } , this works well, meaning - the networkTypeAsFlow does get updated, it just doesn't call the delegates.observable
The Observable delegate monitors changes to the property itself. It is futile to use Observable for a val property, because a val property is never set to a new value. Mutating the object pointed at by the property is completely invisible to the property delegate.
If you want to observe changes, you can launch and collect:
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> = networkTypeState
init {
viewModelScope.launch {
networkTypeAsFlow.collect {
Lg.i("notify subscribers of network update: $it")
notifySubscribers()
}
}
}
An additional benefit here is that notifySubscribers will always be called from the same dispatcher, regardless of which thread the network state was changed from.
I have a Fragment that I want to do a fetch once on its data, I have used distinctUntilChanged() to fetch just once because my location is not changing during this fragment.
Fragment
private val viewModel by viewModels<LandingViewModel> {
VMLandingFactory(
LandingRepoImpl(
LandingDataSource()
)
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val sharedPref = requireContext().getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
val nombre = sharedPref.getString("name", null)
location = name!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
fetchShops(location)
}
private fun fetchShops(localidad: String) {
viewModel.setLocation(location.toLowerCase(Locale.ROOT).trim())
viewModel.fetchShopList
.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Loading -> {
showProgress()
}
is Resource.Success -> {
hideProgress()
myAdapter.setItems(it.data)
}
is Resource.Failure -> {
hideProgress()
Toast.makeText(
requireContext(),
"There was an error loading the shops.",
Toast.LENGTH_SHORT
).show()
}
}
})
}
Viewmodel
private val locationQuery = MutableLiveData<String>()
fun setLocation(location: String) {
locationQuery.value = location
}
val fetchShopList = locationQuery.distinctUntilChanged().switchMap { location ->
liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(Resource.Loading())
try{
emit(repo.getShopList(location))
}catch (e:Exception){
emit(Resource.Failure(e))
}
}
}
Now, if I go to the next fragment and press back, this fires again, I know that maybe this is because the fragment is recreating and then passing a new instance of viewmodel and thats why the location is not retained, but if I put activityViewModels as the instance of the viewmodel, it also happends the same, the data is loaded again on backpress, this is not acceptable since going back will get the data each time and this is not server efficient for me, I need to just fetch this data when the user is in this fragment and if they press back to not fetch it again.
Any clues ?
I'm using navigation components, so I cant use .add or do fragment transactions, I want to just fetch once on this fragment when creating it first time and not refetching on backpress of the next fragment
TL;DR
You need to use a LiveData that emits its event only once, even if the ui re-subscribe to it. for more info and explanation and ways to fix, continue reading.
When you go from Fragment 1 -> Fragment 2, Fragment 1 is not actually destroyed right away, it just un-subscribe from your ViewModel LiveData.
Now when you go back from F2 to F1, the fragment will re-subscribe back to ViewModel LiveData, and since the LiveData is - by nature - state holder, then it will re-emit its latest value right away, causing the ui to rebind.
What you need is some sort of LiveData that won't emit an event that has been emitted before.
This is common use case with LiveData, there's a pretty nice article talking about this need for a similar LiveData for different types of use cases, you can read it here.
Although the article proposed a couple of solutions but those can be a bit of an overkill sometimes, so a simpler solution would be using the following ActionLiveView
// First extend the MutableLiveData class
class ActionLiveData<T> : MutableLiveData<T>() {
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T?>) {
// Being strict about the observer numbers is up to you
// I thought it made sense to only allow one to handle the event
if (hasObservers()) {
throw Throwable("Only one observer at a time may subscribe to a ActionLiveData")
}
super.observe(owner, Observer { data ->
// We ignore any null values and early return
if (data == null) return
observer.onChanged(data)
// We set the value to null straight after emitting the change to the observer
value = null
// This means that the state of the data will always be null / non existent
// It will only be available to the observer in its callback and since we do not emit null values
// the observer never receives a null value and any observers resuming do not receive the last event.
// Therefore it only emits to the observer the single action so you are free to show messages over and over again
// Or launch an activity/dialog or anything that should only happen once per action / click :).
})
}
// Just a nicely named method that wraps setting the value
#MainThread
fun sendAction(data: T) {
value = data
}
}
You can find more explainiation for ActionLiveData in this link if you want.
I would advise using the ActionLiveData class, I've been using it for small to medium project size and it's working alright so far, but again, you know your use cases better than me. :)
I need to implement a search on a large data set that can take some time to complete on mobile devices. So I want to display each matching result as soon as it becomes available.
I need to fetch all available data from a data store that decides whether to get them from network or from the device. This call is an Observable. As soon as the data from that Observable becomes available I want to loop over it, apply a search predicate and notify any Observers for any match found.
So far my idea was to use a PublishSubject to subscribe to and call its onNext function every time the search finds a new match. However I can't seem to get the desired behavior to work.
I'm using MVVM + Android Databinding and want to display every matched entry in a RecyclerView so for every onNext event that is received by the observing viewModel I have to call notifyItemRangeInserted on the RecyclerView's adapter.
class MySearch(val dataStore: MyDataStore) {
private val searchSubject = PublishSubject.create<List<MyDto>>()
fun findEntries(query: String): Observable<List<MyDto>> {
return searchSubject.doOnSubscribe {
// dataStore.fetchAll returns an Observable<List<MyDto>>
dataStore.fetchAll.doOnNext {
myDtos -> if (query.isNotBlank()) {
search(query, myDtos)
} else {
searchSubject.onNext(myDtos)
}
}.subscribe(searchSubject)
}
}
private fun(query: String, data: List<MyDto>) {
data.forEach {
if (it.matches(query)) {
// in real life I cache a few results and don't send each single item
searchSubject.onNext(listOf(it))
}
}
}
fun MyDto.matches(query: String): Boolean // stub
}
-
class MyViewModel(val mySearch: MySearch, val viewNotifications: Observer<Pair<Int, Int>>): BaseObservable() {
var displayItems: List<MyItemViewModel> = listOf()
fun loadData(query: String): Subscription {
return mySearch.findEntries(query)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(this::onSearchResult)
.doOnCompleted(viewNotifications::onCompleted)
.doOnError(viewNotifications::onError)
.subscribe()
}
private fun onSearchResult(List<MyDto> data) {
val lastIndex = displayItems.lastIndex
displayItems = data.map { createItem(it) }
notifyChange()
viewNotifications.onNext(Pair(lastIndex, data.count()))
}
private fun createItem(dto: MyDto): MyItemViewModel // stub
}
The problem I have with the above code is that with an empty query MyViewModel::onSearchResult is called 3 times in a row and when the query is not empty MyViewModel::onSearchResult isn't called at all.
I suspect the problem lies somewhere in the way I have nested the Observables in findEntries or that I'm subscribing wrong / getting data from a wrong thread.
Does anyone have an idea about this?