How to join multiple different observables and subscribe from viewmodel?
I am using single source of truth
principle, so firstly I get data from db then load data from webservice and finally save all data to the db.
For that I used rxjava, room, dagger2, retrofit libraries. But there was a some problem. I must to get
multiple list from webservice and save each list to database. I try some solution, but this code
replies the same request multiple times. Progressbar changes each time. How can I simplify? Best practices for that.
Api.json
{
"data": {
"ad": [
{
"id": 11,
"image": "ad/ru/msG0y8vuXl.png"
}
...
],
"categories": [...],
"status": [...],
"location": [...]
}
}
HomeRepository.kt
class HomeRepository #Inject constructor(
private val indexApi: IndexApi,
private val categoryDao: CategoryDao,
private val userDao: UserDao,
private val adDao: AdDao
) {
fun getCategoryList(): Observable<List<Category>> {
val categoryListDb: Observable<List<Category>> = categoryDao.getCategoryList()
.filter { t: List<Category> -> t.isNotEmpty() }
.subscribeOn(Schedulers.computation())
.toObservable()
val categoryListApi: Observable<List<Category>> = indexApi.getIndex()
.toObservable()
.map { response ->
Observable.create { subscriber: ObservableEmitter<Any> ->
categoryDao.insertCategoryList(response.data.categories)
subscriber.onComplete()
}
.subscribeOn(Schedulers.computation())
.subscribe()
response.data.categories
}
.subscribeOn(Schedulers.io())
return Observable
.concatArrayEager(categoryListDb, categoryListApi)
.observeOn(AndroidSchedulers.mainThread())
}
fun getUserList(): Observable<List<User>> {
// same as above
}
fun getAdList(): Observable<List<Ad>> {
// same as above
}
}
HomeViewmodel.kt
class HomeViewModel #Inject constructor(
private val homeRepository: HomeRepository
) : BaseViewModel() {
private val categoryLiveData: MutableLiveData<Resource<List<Category>>> = MutableLiveData()
private val adLiveData: MutableLiveData<Resource<List<Ad>>> = MutableLiveData()
private val userLiveData: MutableLiveData<Resource<List<User>>> = MutableLiveData()
fun categoryResponse(): LiveData<Resource<List<Category>>> = categoryLiveData
fun adResponse(): LiveData<Resource<List<Ad>>> = adLiveData
fun userResponse(): LiveData<Resource<List<User>>> = userLiveData
fun loadCategory() {
categoryLiveData.postValue(Resource.loading())
compositeDisposable.add(
homeRepository.getCategoryList()
.subscribe({ response ->
categoryLiveData.postValue(Resource.succeed(response))
}, { error ->
categoryLiveData.postValue(Resource.error(error))
})
)
}
fun loadAd() { // Same as above }
fun loadUser() { // Same as above }
}
HomeFragment.kt
fun init(){
// ..
viewmodel.loadCategory()
viewmodel.adResponse()
viewmodel.userResponse()
viewmodel.categoryResponse().observe(this, Observer {
when(it.status){
Status.SUCCEED -> { progressBar.toGone() }
Status.LOADING -> { progressBar.toVisible() }
Status.FAILED -> { progressBar.toGone() }
}
}
viewmodel.adResponse().observe(this, Observer { //Same as above }
viewmodel.userResponse().observe(this, Observer { //Same as above }
}
You should be able to prevent multiple calls from happening in your code, by wrapping indexApi.getIndex().toObservable() inside a connectable observable.
This a bit more of an advanced topic, but roughly what you need to do is:
Create a field in your class HomeRepository:
private val observable = Observable.defer {
indexApi.getIndex().toObservable()
}.replay(1).refCount()
And then, you need to replace every use of indexApi.getIndex().toObservable() with observable.
This might not exactly achieve the result you expected. This blog post seems to be a write-up of other possible options: https://blog.danlew.net/2016/06/13/multicasting-in-rxjava/
Related
New to Android Development (2 months)
have a problem with emitting and collecting data. kinda hart to understand what's going on.
The problem is that, now i am getting a request token which i need to get session ID after logging in.
How can i store this token and then how can i use it in another flow which gives me session id?
Thanks!
i have this in ViewModel.kt
class LoginViewModel(application: Application): AndroidViewModel(application) {
val apiKey = "acdbc7ef61877f0d6b3e29d062218ccc"
private val _loginState = MutableSharedFlow<Resource<TokenResponse>>()
val loginState = _loginState.asSharedFlow()
fun getActualKey(apiKey: String){
viewModelScope.launch {
loginResponse(apiKey).collect{
_loginState.emit(it)
}
}
}
fun loginResponse(apiKey: String) = flow {
val response = RetrofitHelper.tokenService.getRequestToken(apiKey)
if (response.isSuccessful) {
val body = response.body()
Resource.Success(body).let {
emit(Resource.Success(body!!))
}
} else {
val error = response.errorBody()?.string()
emit(Resource.Error(error.toString()))
}
}
}
and this in Fragment where i collect.
class LoginFragment : BaseFragment<FragmentLoginBinding>(FragmentLoginBinding::inflate) {
private val loginViewModel: LoginViewModel by viewModels()
val apiKey = "acdbc7ef61877f0d6b3e29d062218ccc"
override fun viewCreated() {
observe()
}
override fun listeners() {
binding.btnLogin.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
loginViewModel.loginState.collectLatest {
loginViewModel.getActualKey(apiKey)
}
}
}
}
private fun observe() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
loginViewModel.loginState.collectLatest {
when (it) {
is Resource.Success -> {
Log.d("tag", "cool ${it.data.requestToken}")
}
is Resource.Error -> {
Log.d("tag", "error")
}
is Resource.Loading -> {
Log.d("tag", "loading")
}
}
}
}
}
}
}
I don't figured out what you up to
if you want to simply get last emitted value from another flow without using collect you can use MutableStateFlow that always contains latest value in it without suspention
but if you want to combine two (or more) flows into one flow
you can use combine function, it passes latest emitted values to your provided lambda (transform), so you can transform them to one flow
here is a sample
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
fun main() {
val a = flowOf(1, 2, 3)
val b = flowOf("a", "b", "c")
val combinedFlow: Flow<Pair<Int, String>> =combine(a, b) { latestOfA, latestOfB ->
latestOfA to latestOfB
}
}
Full source code can be found here : https://github.com/alirezaeiii/SavingGoals-Cache
This is LocalDataSource class :
#Singleton
class QapitalLocalDataSource #Inject constructor(
private val goalsDao: GoalsDao
) : LocalDataSource {
override fun getSavingsGoals(): Single<List<SavingsGoal>> =
Single.create { singleSubscriber ->
goalsDao.getGoals()
.subscribe {
if (it.isEmpty()) {
singleSubscriber.onError(NoDataException())
} else {
singleSubscriber.onSuccess(it)
}
}
}
}
Above Method has been used in Repository class :
#Singleton
class GoalsRepository #Inject constructor(
private val remoteDataSource: QapitalService,
private val localDataSource: LocalDataSource,
private val schedulerProvider: BaseSchedulerProvider
) {
private var cacheIsDirty = false
fun getSavingsGoals(): Observable<List<SavingsGoal>> {
lateinit var goals: Observable<List<SavingsGoal>>
if (cacheIsDirty) {
goals = getGoalsFromRemoteDataSource()
} else {
val latch = CountDownLatch(1)
var disposable: Disposable? = null
disposable = localDataSource.getSavingsGoals()
.observeOn(schedulerProvider.io())
.doFinally {
latch.countDown()
disposable?.dispose()
}.subscribe({
goals = Observable.create { emitter -> emitter.onNext(it) }
}, { goals = getGoalsFromRemoteDataSource() })
latch.await()
}
return goals
}
}
As you see I am using countDownLatch.await() to make sure result is emmited in subscribe or error block. Is there any better solution than using CountDownLatch while using RxJava?
latch.await() blocks the thread which kinda defeats the whole point of using an async API like RxJava.
RxJava has APIs like onErrorResumeNext to handle exceptions and toObservable to convert a Single result to an Observable result.
Also, RxJava types like this are typically intended to be cold (they don't run or figure anything out until you subscribe) so I'd recommend not checking cacheIsDirty until the subscription happens.
I'd go with something like:
fun getSavingsGoals(): Observable<List<SavingsGoal>> {
return Observable
.fromCallable { cacheIsDirty }
.flatMap {
if (it) {
getGoalsFromRemoteDataSource()
} else {
localDataSource.getSavingsGoals()
.toObservable()
.onErrorResumeNext(getGoalsFromRemoteDataSource())
}
}
}
Btw, if you are already using Kotlin, I highly recommend coroutines. Then you async code ends up reading just like regular sequential code.
My problem is, that when I try to get a document out of my database, that this document aka the object is always null. I only have this problem when I use Kotlin Coroutines to get the document out of my database. Using the standard approach with listeners do work.
EmailRepository
interface EmailRepository {
suspend fun getCalibratePrice(): Flow<EmailEntity?>
suspend fun getRepairPrice(): Flow<EmailEntity?>
}
EmailRepository Implementation
class EmailRepositoryImpl #Inject constructor(private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
// This works! When using flow to write a document, the document is written!
override fun sendEmail(email: Email)= flow {
emit(EmailStatus.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(EmailStatus.success(Unit))
} else {
emit(EmailStatus.failed<Unit>("No Email connection"))
}
}.catch {
emit(EmailStatus.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getCalibratePrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Kalibrieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getRepairPrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Reparieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
}
Viewmodel where I get the data
init {
viewModelScope.launch {
withContext(Dispatchers.IO) {
if (subject.value != null){
when(subject.value) {
"Test" -> {
emailRepository.getCalibratePrice().collect {
emailEntity.value = it
}
}
"Toast" -> {
emailRepository.getRepairPrice().collect {
emailEntity.value = it
}
}
}
}
}
}
}
private val emailEntity = MutableLiveData<EmailEntity?>()
private val _subject = MutableLiveData<String>()
val subject: LiveData<String> get() = _subject
Fragment
#AndroidEntryPoint
class CalibrateRepairMessageFragment() : EmailFragment<FragmentCalibrateRepairMessageBinding>(
R.layout.fragment_calibrate_repair_message,
) {
// Get current toolbar Title and send it to the next fragment.
private val toolbarText: CharSequence by lazy { toolbar_title.text }
override val viewModel: EmailViewModel by navGraphViewModels(R.id.nav_send_email) { defaultViewModelProviderFactory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Here I set the data from the MutableLiveData "subject". I don't know how to do it better
viewModel.setSubject(toolbarText.toString())
}
}
One would say, that the Firebase rules are the problems here, but that should not be the case here, because the database is open and using the listener approach does work.
I get the subject.value from my CalibrateRepairMessageFragment. When I don't check if(subject.value != null) I get a NullPointerException from my init block.
I will use the emailEntitiy only in my viewModel and not outside it.
I appreciate every help, thank you.
EDIT
This is the new way I get the data. The object is still null! I've also added Timber.d messages in my suspend functions which also never get executed therefore flow never throws an error.. With this new approach I don't get a NullPointerException anymore
private val emailEntity = liveData {
when(subject.value) {
"Test" -> emailRepository.getCalibratePrice().collect {
emit(it)
}
"Toast" -> emailRepository.getRepairPrice().collect {
emit(it)
}
// Else block is never executed, therefore "subject.value" is either Test or toast and the logic works. Still error when using flow!
else -> EmailEntity("ERROR", 0F)
}
}
I check if the emailEntity is null or not with Timber.d("EmailEntity is ${emailEntity.value}") in one of my functions.
I then set the price with val price = MutableLiveData(emailEntity.value?.basePrice ?: 1000F) but because emailentity is null the price is always 1000
EDIT 2
I have now further researched the problem and made a big step forward. When observing the emailEntity from a fragment like CalibrateRepairMessageFragment the value is no longer null.
Furthermore, when observing emailEntity the value is also not null in viewModel, but only when it is observed in one fragment! So how can I observe emailEntity from my viewModel or get the value from my repository and use it in my viewmodel?
Okay, I have solved my problem, this is the final solution:
Status class
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<T> : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success<T>(data)
fun <T> loading() = Loading<T>()
fun <T> failed(message: String?) = Failure<T>(message)
}
}
EmailRepository
interface EmailRepository {
fun sendEmail(email: Email): Flow<Status<Unit>>
suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>>
suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>>
}
EmailRepositoryImpl
class EmailRepositoryImpl (private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
override fun sendEmail(email: Email)= flow {
Timber.d("Executed Send Email Repository")
emit(Status.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(Status.success(Unit))
} else {
emit(Status.failed<Unit>("No Internet connection"))
}
}.catch {
emit(Status.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// Sends status and object to viewModel
override suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getCalibrate Price")
emit(Status.failed(it.message.toString()))
}
// Sends status and object to viewModel
override suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getRepairPrice")
emit(Status.failed(it.message.toString()))
}
}
ViewModel
private lateinit var calibrateRepairPrice: CalibrateRepairPricing
private val _calirateRepairPriceErrorState = MutableLiveData<Status<Unit>>()
val calibrateRepairPriceErrorState: LiveData<Status<Unit>> get() = _calirateRepairPriceErrorState
init {
viewModelScope.launch {
when(_subject.value.toString()) {
"Toast" -> emailRepository.getCalibratePrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
else -> emailRepository.getRepairPrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
}
price.postValue(calibrateRepairPrice.head!!.basePrice)
}
}
You can now observe the status in one of your fragments (but you dont need to!)
Fragment
viewModel.calibrateRepairPriceErrorState.observe(viewLifecycleOwner) { status ->
when(status) {
is Status.Success -> requireContext().toast("Price successfully loaded")
is Status.Loading -> requireContext().toast("Price is loading")
is Status.Failure -> requireContext().toast("Error, Price could not be loaded")
}
}
This is my toast extensions function:
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, text, duration).show()
}
hello I'm trying to study dataBinding, mvvm, retrofit and rxjava
in viewModel I used this code
private var mainRepository: MainRepository = MainRepository(NetManager(getApplication()))
val isLoading = ObservableField(false)
var mainModel = MutableLiveData<ArrayList<MainModel>>()
private val compositeDisposable = CompositeDisposable()
fun loadRepositories(id: Int, mainContract: MainContract) {
isLoading.set(true)
compositeDisposable += mainRepository
.getData(id, mainContract)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<ArrayList<MainModel>>() {
override fun onError(e: Throwable) {
//if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(data: ArrayList<MainModel>) {
mainModel.value= data
}
override fun onComplete() {
isLoading.set(false)
}
})
}
and in the MainRepository I used the retrofit with RxJava code
private val model = ArrayList<MainModel>()
fun getData(id: Int, mainContract: MainContract): Observable<ArrayList<MainModel>> {
Api.getData.getMainCategory(id)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({
model.clear()
model.addAll(it)
AppLogger.log("testingModel1", model.toString())
}, {
AppLogger.log("error", "Failed to load Category : $it")
mainContract.toast("Failed to load Category")
})
AppLogger.log("testingModel2", model.toString())
return Observable.just(model)
}
if you notified that I'm using log to see the output data
but what I see is that
AppLogger.log("testingModel2", model.toString())
and
return Observable.just(model)
are running before
Api.getData.getMainCategory(id)
so the output in Logcat testingModel2 first and it is empty then testingModel1 and it is have data
so the result data in
return Observable.just(model)
is nothing
I hope you understand ^_^
Thank you for help
do like:
fun getData(id: Int, mainContract: MainContract): Observable<ArrayList<MainModel>> {
return Api.getData.getMainCategory(id)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
}
but remember to subscribe to it later and add ErrorHandling
And about logs: the problem that actions in subscribe block runs only when Api.getData.getMainCategory(id) emit something, which could take a time.
I'm using MVVM and rxJava and retrofit to send my request.
I have a bottom navigation view which has 5 fragments and in one of them, I have to send a request and after it, the response is delivered, I have to send another request to my server.
this is my ViewModel class :
class MyViewModel: ViewModel() {
val compositeDisposable = CompositeDisposable()
val myFirstReqLiveData = MutableLiveData<myFirstReqModel>()
val mySecondReqLiveData = MutableLiveData<mySecondReqModel>()
fun getFirstReq(token:String){
val firstReqDisposable = RetrofitClientInstance.getRetrofitInterface()
.getFirstReq(token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).singleElement()
.subscribe({
it-> myFirstReqLiveData.value = it
},{
errorFirstReqLiveData.value = it
},{
})
compositeDisposable.add(firstReqDisposable)
}
fun getSecondReq(token:String){
val secondReqDisposable = RetrofitClientInstance.getRetrofitInterface()
.getSecondReq(token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).singleElement()
.subscribe({
it-> mySecondReqLiveData.value = it
},{
errorSecondReqLiveData.value = it
},{
})
compositeDisposable.add(SecondReqDisposable)
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
and in my fragment, I implement this way:
class FirstTabFragment : Fragment() {
private lateinit var myViewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
getFirstReq(myViewModel, token!!)
observeFirstReq(myViewModel)
observeFirstReqError(myViewModel)
observeSecondReq(myViewModel)
observeSecondReqError(myViewModel)
}
fun getFirstReq(viewModel: MyViewModel, token: String) {
viewModel.getFirstReq(token)
}
fun observeFirstReq(viewModel: MyViewModel) {
viewModel.getFirstReqLiveData().observe(this, Observer { myFirstReqModel ->
getSecondReq(myViewModel)
}
}
fun getSecondReq(viewModel: MyViewModel, token: String) {
viewModel.getSecondReq(token)
}
fun observeSecondReq(viewModel: MyViewModel) {
viewModel.getSecondReqLiveData().observe(this, Observer { mySecondReqModel ->
//do some work with my data
}
}
my problem is when I switch my tabs, my second request called several times.
I think I assign a new subscribe every time i reopen my fragment, so it called several times.
how can I fix this issue?!
Create below class
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
in Viewmodel change like this
val myFirstReqLiveData = MutableLiveData<Event<myFirstReqModel>>()
val mySecondReqLiveData = MutableLiveData<Event<mySecondReqModel>>()
in Fragment class
fun observeFirstReq(viewModel: MyViewModel) {
viewModel.getFirstReqLiveData().observe(this, EventObserver { myFirstReqModel ->
getSecondReq(myViewModel)
}
}
change
it-> myFirstReqLiveData.value = it to
it-> myFirstReqLiveData.value = Event(it)
try using this way, if this helps you.
You can also remove getSecondReq(myViewModel) from observer and combine or chain your requests.
https://github.com/ReactiveX/RxJava/wiki/Combining-Observables
Something like this:
val disposable = RetrofitClientInstance.getRetrofitInterface()
.getFirstReq(token)
.doOnError { errorFirstReqLiveData.value = it }
.doOnNext { myFirstReqLiveData.value = it }
.flatMap { t -> getSecondReq(token) }
.doOnError { errorSecondReqLiveData.value = it }
.doOnNext { mySecondReqLiveData.value = it }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).singleElement()
.subscribe()
compositeDisposable.add(disposable)