I get a list of the index composition (102 tickers) and I want to find out detailed information about them, but out of 102 queries, no more than 10 are always executed, and the ticker is randomly selected. All requests are executed via retrofit2 using RxJava3. What could be the problem?
Here is the ViewModel code:
var price: MutableLiveData<CompanyInfoModel> = MutableLiveData()
fun getCompanyInfoObserver(): MutableLiveData<CompanyInfoModel> {
return price
}
fun makeApiCall(ticker: String) {
val retrofitInstance = RetrofitYahooFinanceInstance.getRetrofitInstance().create(RetrofitService::class.java)
retrofitInstance.getCompanyInfo(ticker)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getCompanyInfoObserverRx())
}
private fun getCompanyInfoObserverRx(): Observer<CompanyInfoModel> {
return object : Observer<CompanyInfoModel> {
override fun onComplete() {
// Hide progress bar
}
override fun onError(e: Throwable?) {
price.postValue(null)
}
override fun onNext(t: CompanyInfoModel?) {
price.postValue(t)
}
override fun onSubscribe(d: Disposable?) {
// Show progress bar
}
}
}
Here is the initialization of the model:
companyInfoModel = ViewModelProvider(this).get(CompanyInfoViewModel::class.java)
companyInfoModel.getCompanyInfoObserver().observe(this, Observer<CompanyInfoModel> { it ->
if(it != null) {
retrieveList(Helper.companyInfoToStock(it))
}
else {
Log.e(TAG, "Error in fetching data")
}
})
And here is the request method itself:
fun getCompanyInfo(ticker: String) {
companyInfoModel.makeApiCall(ticker)
}
Thank you, Pawel. The problem really turned out to be in the API limit, I changed the provider and everything started working as it should.
Related
I'm new to kotlin coroutines. I've been trying to run multiple API calls in parallel and then when all the calls are done update my UI and dismiss the loader, but with no success. This is my code
private fun getScoreForType() {
val job = CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
}
runBlocking {
job.join()
// do some ui work
dismissLoader()
}
}
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
I've also tried using async and awaitAll but couldn't make it work either. The loader is always dismissed before all the calls are done. Any help on how I could make this work would be much appreciated
Use Flow and collectData it will works as LiveData.
For example:
val myIntFlow = MutableStateFlow(-1)
Try something like;
in ViewModelMethods
private fun getScoreForType() {
It goes first:
CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
// it means to change value of flow
myIntFlow.value = 1
}
// Now collect data in fragment to change UI
}
// in fragment like:
CoroutineScope(Dispatchers.Main).launch {
// flow will be triggered, on every changed value
viewModel.myIntFlow.collect {
viewModel.methodFromViewModelToChangeUI()
dissmisloader()
myIntFlow.value = -1
}
}
// try the same here as you wish
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
i have more than 5 api calls in one fragment. Doing this, the app get slower in performance loading.
So i planned to make it run parallel with kotlin. How to use executors, threads in my code.
I implemented api in below format.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.callApiOne()
viewModel.callApiTwo()
viewModel.callApiThree()
viewModel.getResponseInViewModel().observe(this.viewLifecycleOwner, Observer {
if (it.errorResponse?.code() == 200) {
}
})
}
ViewModel.kt
fun callApiOne() {
repository.callApiOne()
}
fun getResponseInViewModel(): MutableLiveData<Resource<Model>> {
respp = repository.getResponse()
return respp
}
Repository.kt
private val resp by lazy { MutableLiveData<Resource<Model>>() }
fun callApiOne() {
val api = AppMain.restClient?.services?.callApiOne()
api?.enqueue(object : Callback<Model> {
override fun onFailure(call: Call<Model>, t: Throwable) {
resp.postValue(Resource.failure(t.message!!, null))
}
override fun onResponse(
call: Call<Model>,
response: Response<Model>
) {
if (response.isSuccessful) {
resp.postValue(Resource.successResp(response))
} else {
resp.postValue(Resource.errorresponse(response))
}
}
})
}
fun getResponse(): MutableLiveData<Resource<Model>> = resp
You are supposed to use coroutines in this case.
Example:
MyFragment : Fragment(), CoroutineScope by MainScope() {
...
private fun init() {
launch { // starts a coroutine on main thread
viewModel.fooSuspend()
}
}
}
MyViewModel : ViewModel() {
...
suspend fun fooSuspend() {
withContext(Dispatchers.IO) { //do on IO Thread
doIOHaveOperation()
}
}
}
I have this rating button on my fragment that observes the post request on my viewmodel. The thing is, I would like to tell the user whether the post request was successful or not through a toast but as I have it now, I can only post the request and see if it was successful by the logs.
How can I do that?
This is the button:
private fun ratingButton() {
binding.btnRating.setOnClickListener {
arguments?.getInt("Id")?.let {
arguments?.getString("Session Id").let { it1 ->
if (it1 != null) {
viewModel.postRating(it, mapOf("value" to binding.ratingBar.rating), it1)
}
}
}
}
}
This is the view model:
class RatingViewModel constructor(
private val remoteDataSource: MovieRemoteDataSource
): ViewModel() {
val ratingSuccess = MutableLiveData<Boolean>()
val ratingFailedMessage = MutableLiveData<String?>()
private var _rating = MutableLiveData<Resource<RatingResponse>>()
val rating: LiveData<Resource<RatingResponse>>
get() = _rating
fun postRating(rating:Int, id:Map<String,Float>, session_id:String){
remoteDataSource.postRating(rating, id, session_id, object: MovieRemoteDataSource.RatingCallBack<RatingResponse>{
override fun onSuccess(value: Resource<RatingResponse>){
ratingSuccess.postValue(true)
_rating.value = value
}
override fun onError(message:String?){
ratingSuccess.postValue(false)
ratingFailedMessage.postValue(message)
}
})
}
}
This is the remote data source:
interface RatingCallBack<T> {
fun onSuccess(value: Resource<T>)
fun onError(message: String?)
}
fun postRating(rating: Int, id:Map<String,Float>, session_id:String, ratingCallback: RatingCallBack<RatingResponse>) {
val service = RetrofitService.instance
.create(MovieService::class.java)
val call = service.postRating(rating, session_id, id)
call.enqueue(object : Callback<RatingResponse?> {
override fun onResponse(
call: Call<RatingResponse?>,
response: Response<RatingResponse?>
) {
if (response.isSuccessful) {
Log.d("d", "d")
} else {
Log.d("d", "d")
}
}
override fun onFailure(call: Call<RatingResponse?>, t: Throwable) {
Log.d("d", "d")
}
})
}
I added the resource because I think it might be helpful but it doesn't seem to be working:
data class Resource<out T> (
val status: NetworkStatus,
val data: T? = null,
val message: String? = null
)
enum class NetworkStatus{
LOADING,
SUCCESS,
ERROR
}
You don't call your ratingCallback in the dataSource once the postRating finishes, therefore the results don't get to the ViewModel. Do:
class MovieRemoteDataSource {
fun postRating(ratingCallback: RatingCallBack<...>) {
val service = RetrofitService.instance
.create(MovieService::class.java)
val call = service.postRating()
call.enqueue(object : Callback<...> {
override fun onResponse(
call: Call<...>,
response: Response<...>
) {
if (response.isSuccessful) {
ratingCallback.onSuccess(...)
} else {
ratingCallback.onError(...)
}
}
override fun onFailure(call: Call<...>, t: Throwable) {
ratingCallback.onError(...)
}
})
}
}
If your toasts still won't show after adding the missing callbacks, you probably haven't set up your fragment to observe the liveData correctly yet. First, make sure your binding has LifecycleOwner assigned. Without it, liveData may not be observed:
binding = DataBindingUtil.inflate(...)
// add this line after you inflate the binding
binding.lifecycleOwner = viewLifecycleOwner
Then, observe your ratingSuccess liveData:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// make sure your viewModel has been created at this point, before calling observe()
observe()
}
private fun observe() {
viewModel.ratingSuccess.observe(viewLifecycleOwner, Observer { success ->
if (success) {
Toast.makeText(requireContext(), "Post rating succeeded", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(requireContext(), "Post rating failed", Toast.LENGTH_SHORT).show()
}
})
}
You can observe (in fragment) the value changes of ratingSuccess which is a MutableLiveData but I would strongly suggest to use LiveData for observing purposes.
viewModel.ratingSuccess.observe(viewLifecycleOwner, Observer {
if (it) {
Toast.makeText(this, "Request succeeded", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Request failed", Toast.LENGTH_SHORT).show()
}
})
OR if you're using the latest Kotlin version:
viewModel.ratingSuccess.observe(viewLifecycleOwner) {
if (it) {
Toast.makeText(this, "Request succeeded", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Request failed", Toast.LENGTH_SHORT).show()
}
}
I just need to point out that using a LiveData like this for one-shot events will give you problems, unfortunately. LiveData is designed to always give each observer the most recent value, whereas what you want is to post a result, have one thing observe it once and then it's consumed.
Every time you register an observer on that LiveData, it'll get the last success or fail value, and pop a toast up. So whenever you rotate the device, move away from the app and come back etc, you get a message. And you probably don't want that!
It's unfortunately not a neatly solved problem at this point - here's some reading:
LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) (written by one of the Android team)
Android SingleLiveEvent Redux with Kotlin Flow (recommended by the above link, I've used this approach - uses Flows and Channels to create consumable single events)
Yes it's a huge pain
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()
}
I am working on creating my MVVM template project.
The way MVVM work's is that the View observes changes to the VM and reacts to them. How would i implement that logic in the following example.
View code:
fun addPost(): Unit {
viewModel.addPost(dataSource.text.get(), dataSource.title.get(), { postID: Long ->
if (postID.equals(0)) {
Toast.makeText(MyApp.instance.getContext(), "Error", Toast.LENGTH_LONG).show()
} else {
(arguments.get(ARG_RunOnPositiveDismiss) as (DMPost) -> Unit)(DMPost(postID, MyOptions.User.GET()!!.UserID, dataSource.title.get(), dataSource.text.get()))
dismiss()
}
})
}
ViewModel code:
fun addPost(title:String,text:String,onSuccess: (Long) -> Unit): Unit {
rep.insertPost(DMPost(0, MyOptions.User.GET()!!.UserID, title, text),onSuccess)
}
Repository code:
fun insertPost(post: DMPost, onSuccess: (Long) -> Unit) {
if (MyOptions.Online.GET()!!) {
val client = retrofit.create(APIPost::class.java)
val insertPost: Call<Long> = client.insertPost(post)
insertPost.enqueue(object : Callback<Long> {
override fun onResponse(call: Call<Long>, response: Response<Long>) {
if (response.isSuccessful) {
onSuccess(response.body()!!)
} else {
onSuccess(0)
}
}
override fun onFailure(call: Call<Long>, t: Throwable) {
onSuccess(0)
}
})
} else {
AsyncTask.execute {
try {
onSuccess(daoPost.insertPost(EntityPost(post)))
} catch (e: Exception) {
onSuccess(0)
}
}
}
}
I am assuming that i need to have 2 methods in my view, one for showing the error toast and the other for success.the view should observe a value on the VM and on a change on that value, should call one of those 2 methods.
Is my assumption right?
If yes, can someone write the code for that logic?
In your viewmodel:
private MediatorLiveData<YourDataType> mDataObserved;
public LiveData<YourDataType> getDataObserved(){
return mDataObserved;
}
And in your view:
viewModel.getDataObserved().observer(this, Observer<YourDataType>{
// do update view logic in here
});
Last, in your repository callback call viewmodel to notify data changed:
mDataObserved.postValue(YourDataValue);