RxJava: How to extract same observeOn from different functions? - android

Basically my Android app have some meta data which need to report to backend server in different scenarios:
data class SearchMetaData(
val searchId: String?,
val isRateLimit: Boolean
)
In order to make the code clean, I make the minimal case as follows. All the report logic are similar, each of the function subscribe the metadata provider and get the value that need to report.
fun logEvent1() {
fetchMetaData().observeOn(schedulers.mainThread()).subscribe({ metadata ->
...//lots of other events data here
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
event1.session = sessionMetadata
...
report(event1)
})
}
fun logEvent2() {
fetchMetaData().observeOn(schedulers.mainThread()).subscribe({ metadata ->
...//lots of other events data here
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
event2.session = sessionMetadata
...
report(event2)
})
}
fun logEvent3() {
fetchMetaData().observeOn(schedulers.mainThread()).subscribe({ metadata ->
...//lots of other events data here
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
event3.session = sessionMetadata
...
report(event3)
})
}
My concern is every time we change the schema of metadata, we need to update all the logEventX , I was wondering if maybe we could extract all the subscribe in different functions and get the metadata?

Consider an extension function using compose and doOnSuccess
Single<MetaData>.handleLogging() : Single<MetaData>{
return compose{
it.doOnSuccess{ metaData ->
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
report(sessionMetaData)
}
}
}
//usage
fetchMetaData().handleLogging().subscribe{
//other uncommon logic here.
}

Related

Kotlin MVVM Firestore

How i can remove this carsListener?
Otherwise I get an infinite download
override fun getUserData(userId: String): Flow<Response<User>> = callbackFlow {
Response.Loading
val userClients = arrayListOf<Client>()
val clientsListener = firestore
.collection("users").document(userId).collection("clients")
.addSnapshotListener { clients, e ->
if (clients != null) {
for (client in clients) {
val carsList = arrayListOf<Car>()
val carsListener = firestore
.collection("users").document(userId)
.collection("clients").document(client.id)
.collection("cars")
.addSnapshotListener { cars, error ->
if (cars != null) {
for (car in cars) {
val carsData = car.toObject(Car::class.java)
carsList.add(carsData)
}
}
}
val data = client.toObject(Client::class.java)
userClients.add(data.copy(cars = carsList))
}
} else
Response.Error(e?.message ?: e.toString())
}
val snapShotListener = firestore.collection("users").document(userId)
.addSnapshotListener { user, error ->
val response = if (user != null) {
val userData = user.toObject(User::class.java)
Response.Success<User>(userData?.copy(clients = userClients)!!)
} else
Response.Error(error?.message ?: error.toString())
trySend(response).isSuccess
}
awaitClose {
clientsListener.remove()
snapShotListener.remove()
}
}
I tried everything, but it didn't give any result
if you know how to do it differently, please tell me
My understanding is that you have nested collections: Users > Clients > Cars. In this case, you don't need to add Snapshot Listeners to all those collections. You can simply attach a listener to Users and then call get() to get the data from its subcollections.
Also, starting in version 24.4.0 of the Cloud Firestore Android SDK (BoM 31.0.0) you can take advantage of:
the snapshots() function which returns a Flow<QuerySnapshot> so that you don't have to write your own implementation of CallbackFlow.
the await() function which allows you to await for the result of a CollectionReference.get().
Be sure to update to the latest Firebase version on your app/build.gradle file to be able to use these functions.
And putting it all together should result in a code that looks like this:
override fun getUserData(userId: String): Flow<Response<User>> {
val userRef = firestore.collection("users").document(userId)
val clientsRef = userRef.collection("clients")
return userRef.snapshots().map { documentSnapshot ->
// Convert the DocumentSnapshot to User class
val fetchedUser = documentSnapshot.toObject<User>()!!
// Fetch the clients for that User
val clientsSnapshot = clientsRef.get().await()
// iterate through each client to fetch their cars
val clients = clientsSnapshot.documents.map { clientDoc ->
// Convert the fetched client
val client = clientDoc.toObject<Client>()!!
// Fetch the cars for that client
val carsSnapshot = clientsRef.document(clientDoc.id)
.collection("cars").get().await()
// Convert the fetched cars
val cars = carsSnapshot.toObjects<Car>()
client.copy(cars = cars)
}
Response.Success(fetchedUser.copy(clients = clients))
}.catch { error ->
Response.Error<User>(error.message ?: error.toString())
}
}
As a side note: Reading from all these 3 collections at once can become expensive, depending on the number of Documents that each collection contains. That's due to how Firestore pricing works - you get charged for each document read.
Consider adjusting your database structure to help save costs. Here are some resources that could be helpful:
Cloud Firestore Data Model
Structure Data
Understand Cloud Firestore Billing
Cloud Firestore Pricing | Get to know Cloud Firestore #3
Example Cloud Firestore costs

one-shot operation with Flow in Android

I'm trying to show a user information in DetailActivity. So, I request a data and get a data for the user from server. but in this case, the return type is Flow<User>. Let me show you the following code.
ServiceApi.kt
#GET("endpoint")
suspend fun getUser(#Query("id") id: Int): Response<User>
Repository.kt
fun getUser(id: Int): Flow<User> = flow<User> {
val userResponse = api.getUser(id = id)
if (userResponse.isSuccessful) {
val user = userResponse.body()
emit(user)
}
}
.flowOn(Dispatchers.IO)
.catch { // send error }
DetailViewModel.kt
class DetailViewModel(
private val repository : Repository
) {
val uiState: StateFlow<User> = repository.getUser(id = 369).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User() // empty user
)
}
DetailActivity.kt
class DetailActivity: AppCompatActivity() {
....
initObersevers() {
lifecycleScope.launch {
// i used the `flowWithLifecycle` because the data is just a single object.
viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { state ->
// show data
}
}
}
...
}
But, all of sudden, I just realized that this process is just an one-shot operation and thought i can use suspend function and return User in Repository.kt.
So, i changed the Repository.kt.
Repository.kt(changed)
suspend fun getUser(id: Int): User {
val userResponse = api.getUser(id = id)
return if(userResponse.isSuccessful) {
response.body()
} else {
User() // empty user
}
}
And in DetailViewModel, i want to convert the User into StateFlow<User> because of observing from DetailActivity and I'm going to use it the same way as before by using flowWithLifecycle.
the concept is... i thought it's just one single data and i dind't need to use Flow in Repository. because it's not several items like List.
is this way correct or not??
Yeap, this one-time flow doesn't make any sense - it emits only once and that's it.
You've got two different ways. First - is to create a state flow in your repo and emit there any values each time you're doing your GET request. This flow will be exposed to the use case and VM levels. I would say that it leads to more difficult error handling (I'm not fond of this way, but these things are always arguable, haha), but it also has some pros like caching, you can always show/get the previous results.
Second way is to leave your request as a simple suspend function which sends a request, returns the result of it back to your VM (skipping error handling here to be simple):
val userFlow: Flow<User>
get() = _userFlow
private val _userFlow = MutableStateFlow(User())
fun getUser() = launch(viewModelScope) {
_userFlow.value = repository.getUser()
}
This kind of implementation doesn't provide any cache out of scope of this VM's lifecycle, but it's easy to test and use.
So it's not like there is only one "the-coolest-way-to-do-it", it's rather a question what suits you more for your needs.

How to get LiveData to switch between two other LiveData

I have the following scenario. Podcasts can come from internet or local(db) both are LiveData
// Live
private val _live = MutableLiveData<List<Podcast>>()
val live: LiveData<List<Podcast>> = _live
// Local
val local: LiveData<List<Podcast>> = dao.observePodcasts()
// Combined
val podcasts: LiveData<List<Podcast>> = ...
My question is:- How can i use only one LiveData podcasts such that on demand I can get data from live or local
fun search(query: String) {
// podcasts <- from live
}
fun subcribed() {
// podcasts <- from local
}
You can use MediatorLiveData in this case.
What you need to do with MediatorLiveData is need the LiveData sources to be able to listen for changes to the LiveData source.
Try the following:
YourViewModel.kt
private val _podcasts = MediatorLiveData<List<Podcast>>().apply {
addSource(_live) { dataApi ->
// Or you can do something when `_live` has a change in value.
if(local.value == null) {
this.value = dataApi
}
}
addSource(local) { dataLocal ->
// Or you can do something when `local` has a change in value.
if(_live.value == null) {
this.value = dataLocal
}
}
}
val podcasts: LiveData<List<Podcast>> = _podcasts
MediatorLiveData
I've personally used MediatorLiveData in projects to achieve the same function you're describing.
As quoted directly from the docs since they are pretty straight forward...
Consider the following scenario: we have 2 instances of LiveData, let's name them liveData1 and liveData2, and we want to merge their emissions in one object: liveDataMerger. Then, liveData1 and liveData2 will become sources for the MediatorLiveData liveDataMerger and every time onChanged callback is called for either of them, we set a new value in liveDataMerger.
LiveData liveData1 = ...;
LiveData liveData2 = ...;
MediatorLiveData liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
As already suggested, this can be accomplished with MediatorLiveData. Another option would be using Flows instead of combining LiveData.
val podcasts = combine(local, live) { local, live ->
// Add your implementation of how you would like to combine them
live ?: local
}.asLiveData(viewModelScope.coroutineContext)
If you're using Room, you can simply change the return type to Flow to get a Flow result. And for the MutableLiveData you can replace it with MutableStateFlow.
Using MediatorLiveData didn't suit my needs as I expected because I wanted to be able to switch between local and live whenever I want!
So I did the implementation as follows
enum class Source {
LIVE, LOCAL
}
private val _live = MutableLiveData<List<Podcast>>()
private val _local = dao.observePodcasts()
private val source = MutableLiveData<Source>(Source.LOCAL)
// Universal
val podcasts: LiveData<List<Podcasts>> = source.switchMap {
liveData {
when (it) {
Source.LIVE -> emitSource(_live)
else -> emitSource(_local)
}
}
}
emitSource() removes the previously-added source.
Then I implemented the following two methods
fun goLocal() {
source.postValue(Source.LOCAL)
}
fun goLive() {
source.postValue(Source.LIVE)
}
I then call respected function whenever to observer from live or local storage
One of the usecase
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
viewModel.goLive()
return true
}
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
viewModel.goLocal()
return true
}
})

Android live data transformations on a background thread

I saw this but I'm not sure how to implement it or if this is the same issue, I have a mediator live data that updates when either of its 2 source live datas update or when the underlying data (Room db) updates, it seems to work fine but if the data updates a lot it refreshes a lot in quick succession and I get an error
Cannot run invalidation tracker. Is the db closed?
Cannot access database on the main thread since it may potentially lock the UI for a long period of time
this doesn't happen everytime, only when there are a lot of updates to the database in very quick succession heres the problem part of the view model,
var search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<MutableSet<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
val combinedValues =
MediatorLiveData<Pair<String?, MutableSet<String>?>?>().apply {
addSource(search) {
value = Pair(it, filters.value)
}
addSource(filters) {
value = Pair(search.value, it)
}
}
searchPokemon = Transformations.switchMap(combinedValues) { pair ->
val search = pair?.first
val filters = pair?.second
if (search != null && filters != null) {
searchAndFilterPokemonPager(search, filters.toList())
} else null
}.distinctUntilChanged()
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonPager(search: String, filters: List<String>): LiveData<PagingData<PokemonWithTypesAndSpeciesForList>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
maxSize = 60
)
) {
if (filters.isEmpty()){
searchPokemonForPaging(search)
} else {
searchAndFilterPokemonForPaging(search, filters)
}
}.liveData.cachedIn(viewModelScope)
}
#SuppressLint("DefaultLocale")
private fun getAllPokemonForPaging(): PagingSource<Int, PokemonWithTypesAndSpecies> {
return repository.getAllPokemonWithTypesAndSpeciesForPaging()
}
#SuppressLint("DefaultLocale")
private fun searchPokemonForPaging(search: String): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchPokemonWithTypesAndSpeciesForPaging(search)
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonForPaging(search: String, filters: List<String>): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchAndFilterPokemonWithTypesAndSpeciesForPaging(search, filters)
}
the error is actually thrown from the function searchPokemonForPaging
for instance it happens when the app starts which does about 300 writes but if I force the calls off the main thread by making everything suspend and use runBlocking to return the Pager it does work and I don't get the error anymore but it obviously blocks the ui, so is there a way to maybe make the switchmap asynchronous or make the searchAndFilterPokemonPager method return a pager asynchronously? i know the second is technically possible (return from async) but maybe there is a way for coroutines to solve this
thanks for any help
You can simplify combining using combineTuple (which is available as a library that I wrote for this specific purpose) (optional)
Afterwards, you can use the liveData { coroutine builder to move execution to background thread
Now your code will look like
val search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<Set<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
searchPokemon = combineTuple(search, filters).switchMap { (search, filters) ->
liveData {
val search = search ?: return#liveData
val filters = filters ?: return#liveData
withContext(Dispatchers.IO) {
emit(searchAndFilterPokemonPager(search, filters.toList()))
}
}
}.distinctUntilChanged()
}

Change Data type in the RxJava2 chain

I am new to RxJava2 and its methods.
I need to change the data type which is emitted from an Observable.
Say, I have a data class like below.
data class SomeClass (val type: String)
An API returns ArrayList<SomeClass>, this works fine on the current implementation using RxJava2 and RxAndroid.
apiService.getPrice(code)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(singleObserver)
I have to change/transform the data from ArrayList<SomeClass> to HashMap<someClassObject.type, ArrayList<SomeClass>>. I am trying operators to make this happen, but no operator will allow changing the datatype being observed.
Transform based on:
Consumer<ArrayList<SomeClass>> { response ->
val mapped = HashMap<String, ArrayList<SomeClass>>()
response.forEach { someClassObj ->
val type = someClassObj.type!!
if (mapped.containsKey(type)) {
mapped[district]?.add(someClassObj)
} else {
val list = ArrayList<SomeClass>()
list.add(someClassObj)
mapped[type] = list
}
}
}
I am thinking to use two different observables, in which Observable data #2 is based on the response of Observable data #1 (ArrayList<SomeClass>). But, I am not sure if this works. Any better or efficient way to achieve this?
Try map:
apiService.getPrice(code)
.map { response ->
val mapped = HashMap<String, ArrayList<SomeClass>>()
response.forEach { someClassObj ->
val type = someClassObj.type!!
if (mapped.containsKey(type)) {
mapped[district]?.add(someClassObj)
} else {
val list = ArrayList<SomeClass>()
list.add(someClassObj)
mapped[type] = list
}
}
mapped
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(singleObserver);

Categories

Resources