Android: Firebase Object is null when using kotlin flow - android

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()
}

Related

MutableSharedFlow not trigger same event twice in kotlin

I am using MutableSharedFlow in project. My main project concept is very big, so I cannot add in here, instead I made a very small sample to reproduce my problem. I know this example is very wrong, but I have same scenario in my main project. I am using MutableSharedFlow as a Queue implementation with single Thread execution with the help of Mutex.
ExampleViewModel
class ExampleViewModel : ViewModel() {
val serviceNumber = ServiceNumber()
val serviceNumberEventFlow = serviceNumber.eventFlow
val mutex = Mutex()
var delayCounter = 0
suspend fun addItem(itemOne: Int = 2, itemTwo: Int = 2): Add {
return mutex.queueWithTimeout("add") {
serviceNumberEventFlow.onSubscription {
serviceNumber.add(itemOne, itemTwo)
delayCounter++
if (delayCounter == 1) {
delay(1000)
Log.w("Delay ", "Delay Started")
serviceNumber.add(8, 8)
}
}.firstOrNull {
it is Add
} as Add? ?: Add("No value")
}
}
suspend fun subItem(itemOne: Int = 2, itemTwo: Int = 2): Sub {
return mutex.queueWithTimeout("sub") {
serviceNumberEventFlow.onSubscription {
serviceNumber.sub(itemOne, itemTwo)
}.firstOrNull {
it is Sub
} as Sub? ?: Sub("No value")
}
}
private suspend fun <T> Mutex.queueWithTimeout(
action: String, timeout: Long = 5000L, block: suspend CoroutineScope.() -> T
): T {
return try {
withLock {
return#withLock withTimeout<T>(timeMillis = timeout, block = block)
}
} catch (e: Exception) {
Log.e("Wrong", " $e Timeout on BLE call: $action")
throw e
}
}
}
class ServiceNumber : Number {
val eventFlow = MutableSharedFlow<Event>(extraBufferCapacity = 50)
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun add(itemOne: Int, itemTwo: Int) {
Log.i("ServiceNumber", " Add event trigger with $itemOne -- $itemTwo")
eventFlow.emitEvent(Add("Item added ${itemOne + itemTwo}"))
}
override fun sub(itemOne: Int, itemTwo: Int) {
eventFlow.emitEvent(Sub("Item subtract ${itemOne - itemTwo}"))
}
private fun <T> MutableSharedFlow<T>.emitEvent(event: T) {
scope.launch { emit(event) }
}
}
interface Number {
fun add(itemOne: Int, itemTwo: Int)
fun sub(itemOne: Int, itemTwo: Int)
}
sealed class Event
data class Add(val item: String) : Event()
data class Sub(val item: String) : Event()
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val viewModel: ExampleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Theme {
Column {
Button(onClick = {
lifecycleScope.launchWhenCreated {
withContext(Dispatchers.IO) {
val result = viewModel.addItem()
Log.e("Result", "$result")
}
}
}) {
Text("Add")
}
Button(onClick = {
lifecycleScope.launchWhenCreated {
withContext(Dispatchers.IO) {
val result = viewModel.subItem()
Log.e("Result", "$result")
}
}
}) {
Text("Sub")
}
}
}
}
}
}
#Composable
fun Theme(content: #Composable () -> Unit) {
MaterialTheme(content = content)
}
Problem
This example is simple Add and subtract of two number. When I am click on Add Button first time, viewmodel.addItem(...) -> ... ->ServiceNumber.add() will trigger and emit the value and we can see log in console. Inside the Add Button function, I was also added a delay to trigger ServiceNumber.add() again to see that onSubscription will be also retrigger or not. MutableSharedFlow emit the value as I can see in log but onSubscription method not called. I don't understand what is the problem in here.
onSubscription is an operator so it creates a new copy of your shared flow. The lambda code will only be run when there are new collectors on this new flow. The only time you collect this new flow is when you call firstOrNull() on it, a terminal operator that collects a single value.

StateFlow: Cancellation of Older Emitted State After Collecting

I m relatively new in kotlin flows and I m creating the Login Module using Flows in android. I have been stuck from past few days in flows as I m collecting it in ViewModels but I m facing problem when requesting with wrong Credentials its caching all the state. After entering the right credentials the user navigate to main Activity but the instance of the MainActivity is being created with every emitted State: Example(User Enter 3 wrong Credential and 1 Right Credential: 4 Instance of MainActivity Created). So, Is there any way that I can cancel the previous emit and only show the latest request. I m using the collectLatest as well but its not working too. Below is the code.
LoginActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mViewBinding.root)
loginListener()
}
override fun onStart() {
super.onStart()
initViews()
handleNetworkChanges()
}
private fun observeLogin() {
lifecycleScope.launchWhenCreated {
mViewModel.loginCredentials.collect { state ->
when(state){
is State.Loading -> {
showLoading()
}
is State.Success -> {
Timber.d("I m in Success" + state.data)
val intent = Intent(this#LoginActivity,MainActivity::class.java)
startActivity(intent)
closeLoading()
finish()
}
is State.Error -> {
val errorResponse = gson.fromJson(state.message,LoginResponse::class.java)
showToast(errorResponse.messages)
closeLoading()
}
}
}
}
}
private fun loginListener() {
mViewBinding.neumorphButtonSignIn.setOnClickListener {
observeLogin()
phoneNumber = mViewBinding.edtPhoneNumber.text.toString()
pin = mViewBinding.oldPIN.text.toString()
if (phoneNumber.isValidPhone()) {
sendLoginCredentials(phoneNumber ,pin)
}
else {
mViewBinding.edtPhoneNumber.snack("Please Enter valid phone number") {
action("ok") {
dismiss()
}
}
}
}
}
private fun sendLoginCredentials(phoneNumber: String , pin: String) = mViewModel.postLoginCredentials("03XXXX" , "1234")
LoginViewModel
#ExperimentalCoroutinesApi
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginRepository: LoginRepository,
) : ViewModel() {
private val _loginCredentials: MutableStateFlow<State<LoginResponse>> = MutableStateFlow(State.Empty())
val loginCredentials: StateFlow<State<LoginResponse>> get() = _loginCredentials
fun postLoginCredentials(phoneNumber: String, pin: String) {
Timber.d("postLoginCredentials: $phoneNumber + $pin")
_loginCredentials.value = State.loading()
viewModelScope.launch {
loginRepository.login(LoginRequest(phoneNumber,pin))
.map { response -> State.fromResource(response) }
.collect{state -> _loginCredentials.value = state }
}
}
}
LoginRepository
class LoginRepository #Inject constructor(
private val apiInterface: APIInterface
) {
fun login(loginRequest: LoginRequest): Flow<ResponseAPI<LoginResponse>> {
return object : NetworkBoundRepository<LoginRequest, LoginResponse>() {
override suspend fun fetchFromRemote(): Response<LoginResponse> = apiInterface.createLoginRequest(
loginRequest
)
}.asFlow()
}
NetworkBoundRepository
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<ResponseAPI<REQUEST>> {
val apiResponse = fetchFromRemote()
val remotePosts = apiResponse.body()
if (apiResponse.isSuccessful && remotePosts != null) {
emit(ResponseAPI.Success(remotePosts))
} else {
// Something went wrong! Emit Error state.
emit(ResponseAPI.Failed(apiResponse.errorBody()!!.string()))
}
}.catch { e ->
e.printStackTrace()
emit(ResponseAPI.Failed("Network error! Can't get latest posts."))
}
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
Is there any way that I can create One Instance of MainAcitivity while ignoring the older emitted Responses? Any Operator which can work. Any help in this regard is highly appreciated. Thanks.
Actually, I was calling the observeLogin() from the login click Listener which was creating this mess in my project when I move this to onCreate(). Everything works the way as intended. So, posting this for newbie that won't stuck into this.

MVVM Add to favourites functionality

I am implementing an "Add to favourites" functionality in a detail screen. If the user taps the FAB, I want to set the fab as selected and update my database. How could I use the same value that I am sending to the database to be used in my fragment (to be consistent, in case there is some issue while updating the DB)
Fragment
class BeerDetailsFragment : Fragment(R.layout.fragment_beer_details) {
private val viewModel by viewModels<BeerDetailsViewModel>()
private val args by navArgs<BeerDetailsFragmentArgs>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToObservers()
viewModel.getBeer(args.beerId)
}
private fun subscribeToObservers() {
viewModel.beer.observe(viewLifecycleOwner, { resource ->
when(resource.status) {
Status.SUCCESS -> {
loadData(resource.data)
}
Status.ERROR -> {
showError(resource.message)
}
Status.LOADING -> {}
}
})
}
private fun loadData(beerDetails: BeerDomainModel?) {
if (beerDetails != null) {
Glide.with(requireContext())
.load(beerDetails.imageMedium)
.placeholder(R.drawable.ic_beer)
.error(R.drawable.ic_beer)
.fallback(R.drawable.ic_beer)
.into(beerDetailsImage)
beerDetailsName.text = beerDetails.name
beerDetailsDescription.text = beerDetails.description
fab.isSelected = beerDetails.isFavourite
fab.setOnClickListener {
viewModel.updateBeer(beerDetails)
// I shouldn't do it like this in case there is an issue while updating the DB
fab.isSelected = !beerDetails.isFavourite
}
}
}
...
View Model class
class BeerDetailsViewModel #ViewModelInject constructor(private val repository: BreweryRepository) : ViewModel() {
private val beerId = MutableLiveData<String>()
fun getBeer(id: String) {
beerId.value = id
}
var beer = beerId.switchMap { id ->
liveData {
emit(Resource.loading(null))
emit(repository.getBeer(id))
}
}
fun updateBeer(beer: BeerDomainModel) {
viewModelScope.launch {
repository.updateBeer(beer)
}
}
}
Repository
class BreweryRepository #Inject constructor(private val breweryApi: BreweryApi, private val beerDao: BeerDao, private val responseHandler: ResponseHandler) {
...
suspend fun getBeer(id: String): Resource<BeerDomainModel> {
return try {
withContext(IO) {
val isInDB = beerDao.isInDB(id)
if (!isInDB) {
val response = breweryApi.getBeer(id).beer.toDomainModel()
beerDao.insert(response.toBeerEntity())
responseHandler.handleSuccess(response)
} else {
val beer = beerDao.get(id).toDomainModel()
responseHandler.handleSuccess(beer)
}
}
} catch (e: Exception) {
responseHandler.handleException(e)
}
}
suspend fun updateBeer(beer: BeerDomainModel) {
withContext(IO) {
val dbBeer = with(beer) {
copy(isFavourite = !isFavourite)
toBeerEntity()
}
beerDao.update(dbBeer)
}
}
}
I would prefer to use a unidirectional flow with the following implementation:
Not sure how is your DAO implemented, but if you are using Room you could update your get method to return a Flow instead. That way whenever your data is updated, you will get back the updated data.
Then in your VM you just get that Flow or stream of data and subscribe to the updates. Flow has a very convenient method: asLiveData() so your code will look much cleaner.
If you are not using Room, then what I'd do is either construct a Flow or some type of stream and on updates successful updates send out the new data.

Android Room with LiveData + ViewModel Refresh Question

I have a small app I am using to try learn more about some of the newer Android components. I'm finding it difficult to find information and understand how best to do what I want.
Currently: Open app -> load data + stores in DB -> display data in list
I want to be able to query data again upon button press.
I have 2 buttons, 1 to fetch data again, 1 to delete the list data from the DB.
Problem is that it seems you cannot refresh if you are observing on an instance of LiveData, which I am. I understand that however the way I found to actually do a Network call and store in the Database returns an instance of LiveData and I am not sure how best to proceed.
Let me show you the code.
Fragment
private val viewModel: quoteViewModel by viewModels()
private lateinit var binding: FragmentHomeBinding
private lateinit var adapter: QuoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerView()
setupRetrieveQuotesObserver()
setupDeleteDataListener()
setupFetchNewDataListener()
setupSwipeToRefresh()
}
private fun initRecyclerView() {
adapter = QuoteAdapter()
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
}
private fun setupDeleteDataListener() {
binding.removeQuotesButton.setOnClickListener {
viewModel.removeAllQuotes()
}
}
private fun setupFetchNewDataListener() {
binding.getQuotesButton.setOnClickListener {
viewModel.removeQuotes()
viewModel.getQuotes()
}
}
private fun setupRetrieveQuotesObserver() {
viewModel.quoteLiveDataList.observe(viewLifecycleOwner, Observer { result ->
when (result.status) {
NewResult.Status.SUCCESS -> {
result.data.let { adapter.setItems(ArrayList(result.data)) }
binding.progressBar.visibility = View.GONE
binding.swipeContainer.isRefreshing = false
}
NewResult.Status.ERROR -> {
binding.progressBar.visibility = View.GONE
Snackbar.make(binding.root, "Some error has occurred", Snackbar.LENGTH_SHORT)
.show()
}
NewResult.Status.LOADING -> {
binding.progressBar.visibility = View.VISIBLE
}
}
})
}
private fun setupSwipeToRefresh() {
binding.swipeContainer.setOnRefreshListener {
viewModel.getQuotes()
}
}
ViewModel
val quoteLiveDataList: LiveData<NewResult<List<Quote>>> = repository.quotes
fun getQuotes() = viewModelScope.launch {
repository.quotes
}
fun removeAllQuotes() = viewModelScope.launch {
repository.deleteAllQuotes()
}
Repository
val quotes = performGetOperation(
databaseQuery = { dao.getAllQuotes() },
networkCall = { remoteSource.getAllQuotes() },
saveCallResult = {
val quotesList = ArrayList<Quote>()
for (messageString in it.messages.non_personalized) {
quotesList.add(
Quote(
messageString,
FaceImageProvider().getRandomFacePicture(),
false
)
)
}
dao.insertQuotes(quotesList)
}
)
#WorkerThread
suspend fun deleteAllQuotes() = withContext(Dispatchers.IO) { dao.deleteAllQuotes() }
performGetOperation
This is a class I saw online for handling what I want to do. I think the issue stems from here as it is returning LiveData, I'm not sure how best to fix it
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> NewResult<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<NewResult<T>> =
liveData(Dispatchers.IO) {
emit(NewResult.loading())
val source = databaseQuery.invoke().map { NewResult.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == NewResult.Status.SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == NewResult.Status.ERROR) {
emit(NewResult.error(responseStatus.message!!))
emitSource(source)
}
}
RemoteDataSource
suspend fun getQuotes() = getResult { service.getQuotes() }
getResult
protected suspend fun <T> getResult(call: suspend () -> Response<T>): NewResult<T> {
try {
val response = call.invoke()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
return NewResult.success(body)
}
}
return error("${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): NewResult<T> {
Log.d("BaseDataSource", message)
return NewResult.error("Network called failed due to: $message")
}
NewResult
data class NewResult<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING,
}
companion object {
fun <T> success(data: T): NewResult<T> {
return NewResult(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): NewResult<T> {
return NewResult(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): NewResult<T> {
return NewResult(Status.LOADING, data, null)
}
}
Apologies for the very long message, but I guess I need to show all the little bits and bobs I'm using.
I think the problem is in the Fragment where I do viewModel.quoteLiveDataList.observe, as it is returning a new LiveData if it is called again. So I'm not sure how I can do another server call and update the DB and return it here.
Appreciate any help!
Thanks
Use Transformations.switchMap on a MutableLiveData to trigger your repository call like it is done here in the GithubBrowserSample project. This will allow you to implement the refresh functionality -
private val _getQuotes = MutableLiveData<Boolean>()
val quotes: LiveData<NewResult<List<Quote>>> = _getQuotes.switchMap { getQuotes ->
repository.quotes
}
fun getQuotes() {
_getQuotes.value = true
}
fun refresh() {
_getQuotes.value?.let {
_getQuotes.value = it
}
}

request in another request called several times with rxJava and retrofit

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)

Categories

Resources