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) {
}
})
}
Can't understand why the flow block doesn't execute and control returns back to ViewModel.
ViewModel code
fun getFilesFromServer() {
viewModelScope.launch(Dispatchers.IO) {
ftpRepository.getFilesFromServer()
.onEach { state ->
when (state) {
is Resource.Loading -> {
liveData.postValue(Resource.Loading())
}
}
}
}
Repository (Here the problem, control doesn't go inside flow {} block)
override suspend fun getFilesFromServer(): Flow<Resource<Response>> = flow {
emit(Resource.Loading())
ftpClient.connect(
Constants.SERVER_IP, Constants.PORT,
Constants.USER_NAME,
Constants.PASSWORD,
object : OnEZFtpCallBack<Void?> {
override fun onSuccess(response: Void?) {
requestFtpFileList()
}
override fun onFail(code: Int, msg: String) {
}
}
)
}
Thanks for your time...
Forgot to launch the flow
launchIn(this)
Complete code
fun getFilesFromServer() {
viewModelScope.launch(Dispatchers.IO) {
ftpRepository.getFilesFromServer()
.onEach { state ->
when (state) {
is Resource.Loading -> {
liveData.postValue(Resource.Loading())
}
}
}.launchIn(this)
}
I would like to implement a feature like loadStateFlow in Paging 3.
I do not use pagination in my implementation and it is not necessary in my case.
Could I make it another way?
I have found something like LoadingStateAdapter library
https://developer.android.com/reference/kotlin/androidx/paging/LoadStateAdapter
For now I get a list using method in the fragment:
private fun collectNotificationItems() {
vm.notificationData.collectWith(viewLifecycleOwner) {
notificationAdapter.items = it
}
}
This is implementation I would like to achieve, example is in paging3:
private fun collectItems() {
vm.items.collectWith(viewLifecycleOwner, adapter::submitData)
adapter.loadStateFlow.collectWith(viewLifecycleOwner) { loadState ->
vm.setLoadingState(loadState.refresh is LoadState.Loading)
val isEmpty =
loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && archiveAdapter.itemCount < 1
vm.setEmptyStateVisible(isEmpty)
}
}
Where methods are:
in ViewModel
fun setLoadingState(isLoading: Boolean) {
_areShimmersVisible.value = isLoading && !_isSwipingToRefresh.value
if (!isLoading) _isSwipingToRefresh.value = false
}
areShimmers and isSwiping are MutableStateFlow
Could you recommend any other options?
EDIT:
I have the whole implementation a little bit different.
I have use case to make it
class GetListItemDetailsUseCase #Inject constructor(private val dao: Dao): BaseFlowUseCase<Unit, List<ItemData>>() {
override fun create(params: Unit): Flow<List<ItemData>> {
return flow{
emit(dao.readAllData())
}
}
}
For now it looks like the code above.
How to use DateState in that case?
EDIT2:
class GetNotificationListItemDetailsUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseFlowUseCase<Unit, DataState<List<NotificationItemsResponse.NotificationItemData>>>() {
override fun create(params: Unit): Flow<DataState<List<NotificationItemsResponse.NotificationItemData>>> {
return flow{
emit(DataState.Loading)
try {
emit(DataState.Success(notificationDao.readAllDataState()))
} catch(e: Exception) {
emit(DataState.Error(e)) // error, and send the exception
}
}
}
}
DAO
#Query("SELECT * FROM notification_list ORDER BY id ASC")
abstract suspend fun readAllDataState(): DataState<List<NotificationItemsResponse.NotificationItemData>>
/\ error beacause of it:
error: Not sure how to convert a Cursor to this method's return type
fragment
private suspend fun collectNotificationItems() {
vm.notificationData.collectLatest { dataState ->
when(dataState) {
is DataState.Error -> {
collectErrorState()
Log.d("collectNotificationItems", "Collect ErrorState")
}
DataState.Loading -> {
Log.d("collectNotificationItems", "Collect Loading")
}
is DataState.Success<*> -> {
vm.notificationData.collectWith(viewLifecycleOwner) {
notificationAdapter.items = it
notificationAdapter.notifyDataSetChanged()
Log.d("collectNotificationItems", "Collect Sucess")
}
}
}
}
You could use a utility class (usually called DataState or something like that).
sealed class DataState<out T> {
data class Success<out T>(val data: T) : DataState<T>()
data class Error(val exception: Exception) : DataState<Nothing>()
object Loading : DataState<Nothing>()
}
Then, you change your flow's return type from Flow<YourObject> to Flow<DataState<YourObject>> and emit the DataStates within a flow {} or channelFlow {} block.
val notificationsFlow: Flow<DataState<YourObject>> get() = flow {
emit(DataState.Loading) // when you collect, you will receive this DataState telling you that it's loading
try {
// networking/database stuff
emit(DataState.Success(yourResultObject))
} catch(e: Exception) {
emit(DataState.Error(e)) // error, and send the exception
}
}
Finally, just change your collect {} to be like:
notificationsFlow.collectLatest { dataState ->
when(dataState) {
is DataState.Error -> { } // error occurred, deal with it here
DataState.Loading -> { } // it's loading, show progress bar or something
is DataState.Success -> { } // data received from the flow, access it with dataState.data
}
}
For more information on this regard, check this out.
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
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.