Loading indicator does not hide if api failed to retrieve data - android

I want to show a loading indicator at the beginning of the first api call when list of items is being fetched from the server. Everything is okay if data is fetched successfully. That means the loading indicator got invisible if data loaded successfully. The code is given bellow.
must use this code in view model
protected open fun <T> Single<T>.asPageLoadEventSource(eventId: Int = Random.nextInt()): Single<T> {
var emitter: SingleEmitter<Any>? = null
val single = Single.create<Any> { emitter = it }
pageLoadEventSingles[eventId] = single
return this.doOnSuccess { emitter?.onSuccess(it as Any) }
.doOnError { emitter?.tryOnError(it) }
}
view model
private fun getIncomingStiDest() {
val queries = mapOf(
"last_status" to stiDest,
"type" to incoming
)
getStiDestActionLiveData.value = Resource(Status.LOADING)
getDashboardCounterUseCase.execute(queries).asPageLoadEventSource().subscribe({
getStiDestActionLiveData.value = Resource(Status.SUCCESS, it)
}, {
getStiDestActionLiveData.value = Resource(Status.ERROR, it)
}).collect()
}

Related

How to schedule an API request asynchronously for one composable screen from another composable screen? (Jetpack Compose)

I'm a junior Android developer and trying to build a Facebook-like social media app. My issue is that when I bookmark a post in Screen B and the action succeeds, (1) I want to launch an API request in Screen A while in Screen B and (2) update the bookmarked icon ONLY for that particular post.
For the second part of the issue, I tried these two solutions.
I relaunched a manual API request on navigating back to Screen A. This updates the whole list when there's only one small change, hence very inefficient.
I built another URL route to fetch that updated post only and launched it on navigating back to Screen A. But to insert the newly updated post at the old index, the list has to be mutable and I ain't sure this is a good practice.
Please help me on how to solve this issue or similar issues. I'm not sure if this should be done by passing NavArg to update locally and then some or by using web sockets. Thanks in advance.
data class ScreenAState(
val posts: List<Post> = emptyList(),
val isLoading: Boolean = false)
data class ScreenBState(
val post: PostDetail? = null,
val isBookmarked: Boolean? = null)
data class Post(
val title: String,
val isBookMarked: Boolean,
val imageUrl: String)
data class PostDetail(
val title: String,
val content: String,
val isBookMarked: Boolean,
val imageUrl: String)
I suggest you continue with using your logic that will update your list on return from screen B to screen A, but instead of using simple list, you could use:
https://developer.android.com/reference/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList
This list is designed for what you need I think. Update just that one element.
In mean time, you can change that item from list to some loading dummy item, if you want to have loading like view while you wait for API call to finish.
The problem is how to handle data consistency, which is not directly related to jetpack compose. I suggest you solve this problem at the model level. Return flow instead of static data in the repository, and use collectAsState in the jetpack compose to monitor data changes.
It's hard to give an example, because it depends on the type of Model layer. If it's a database, androidx's room library supports returning flow; if it's a network, take a look at this.
https://gist.github.com/FishHawk/6e4706646401bea20242bdfad5d86a9e
Triggering a refresh is not a good option. It is better to maintain an ActionChannel in the repository for each list that is monitored. use the ActionChannel to modify the list locally to notify compose of the update.
For example, you can make a PagedList if the data layer is network. With onStart and onClose, channels can be added or removed from the repository, thus giving the repository the ability to update all the observed lists.
sealed interface RemoteListAction<out T> {
data class Mutate<T>(val transformer: (MutableList<T>) -> MutableList<T>) : RemoteListAction<T>
object Reload : RemoteListAction<Nothing>
object RequestNextPage : RemoteListAction<Nothing>
}
typealias RemoteListActionChannel<T> = Channel<RemoteListAction<T>>
suspend fun <T> RemoteListActionChannel<T>.mutate(transformer: (MutableList<T>) -> MutableList<T>) {
send(RemoteListAction.Mutate(transformer))
}
suspend fun <T> RemoteListActionChannel<T>.reload() {
send(RemoteListAction.Reload)
}
suspend fun <T> RemoteListActionChannel<T>.requestNextPage() {
send(RemoteListAction.RequestNextPage)
}
class RemoteList<T>(
private val actionChannel: RemoteListActionChannel<T>,
val value: Result<PagedList<T>>?,
) {
suspend fun mutate(transformer: (MutableList<T>) -> MutableList<T>) =
actionChannel.mutate(transformer)
suspend fun reload() = actionChannel.reload()
suspend fun requestNextPage() = actionChannel.requestNextPage()
}
data class PagedList<T>(
val list: List<T>,
val appendState: Result<Unit>?,
)
data class Page<Key : Any, T>(
val data: List<T>,
val nextKey: Key?,
)
fun <Key : Any, T> remotePagingList(
startKey: Key,
loader: suspend (Key) -> Result<Page<Key, T>>,
onStart: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null,
onClose: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null,
): Flow<RemoteList<T>> = callbackFlow {
val dispatcher = Dispatchers.IO.limitedParallelism(1)
val actionChannel = Channel<RemoteListAction<T>>()
var listState: Result<Unit>? = null
var appendState: Result<Unit>? = null
var value: MutableList<T> = mutableListOf()
var nextKey: Key? = startKey
onStart?.invoke(actionChannel)
suspend fun mySend() {
send(
RemoteList(
actionChannel = actionChannel,
value = listState?.map {
PagedList(
appendState = appendState,
list = value,
)
},
)
)
}
fun requestNextPage() = launch(dispatcher) {
nextKey?.let { key ->
appendState = null
mySend()
loader(key)
.onSuccess {
value.addAll(it.data)
nextKey = it.nextKey
listState = Result.success(Unit)
appendState = Result.success(Unit)
mySend()
}
.onFailure {
if (listState?.isSuccess != true)
listState = Result.failure(it)
appendState = Result.failure(it)
mySend()
}
}
}
var job = requestNextPage()
launch(dispatcher) {
actionChannel.receiveAsFlow().flowOn(dispatcher).collect { action ->
when (action) {
is RemoteListAction.Mutate -> {
value = action.transformer(value)
mySend()
}
is RemoteListAction.Reload -> {
job.cancel()
listState = null
appendState = null
value.clear()
nextKey = startKey
mySend()
job = requestNextPage()
}
is RemoteListAction.RequestNextPage -> {
if (!job.isActive) job = requestNextPage()
}
}
}
}
launch(dispatcher) {
Connectivity.instance?.interfaceName?.collect {
if (job.isActive) {
job.cancel()
job = requestNextPage()
}
}
}
awaitClose {
onClose?.invoke(actionChannel)
}
}
And in repository:
val postListActionChannels = mutableListOf<RemoteListActionChannel<Post>>()
suspend fun listPost() =
daoFlow.filterNotNull().flatMapLatest {
remotePagingList(
startKey = 0,
loader = { page ->
it.mapCatching { dao ->
/* dao function, simulate network operation, return List<Post> */
dao.listPost(page)
}.map { Page(it, if (it.isEmpty()) null else page + 1) }
},
onStart = { postListActionChannels.add(it) },
onClose = { postListActionChannels.remove(it) },
)
}
suspend fun markPost(title: String) =
oneshot {
/* dao function, simulate network operation, return Unit */
it.markPost(title)
}.onSuccess {
postListActionChannels.forEach { ch ->
ch.mutate { list ->
list.map {
if (it.title == title && !it.isBookMarked)
it.copy(isBookMarked = true)
else it
}.toMutableList()
}
}
}

Wait for first Flow emit and update second Flow whenever there is a change on first one

I implemented an API call and I would like to allow the user to choose the sort order, keeping the choice saved in Datastore Preferences.
But I couldn't implement this idea, since both are returning a Flow, I don't know how to make one Flow wait for the other, and whenever the first one emits a new value, update the second.
Some example code I'm trying:
interface PreferencesRepository {
fun getFilter(): Flow<OrderType>
}
class PreferencesRepositoryImpl #Inject constructor() : PreferencesRepository {
override fun getFilter(): Flow<OrderType> = flow {
delay(timeMillis = 300) // simulate initial read delay from datastore
emit(value = OrderType.ASCENDING)
delay(timeMillis = 3000) // simulate a order change after 3 secs
emit(value = OrderType.DESCENDING)
}
}
interface ItemsRepository {
fun getItems(order: OrderType): Flow<List<ItemModel>>
}
class ItemsRepositoryImpl #Inject constructor() : ItemsRepository {
private val dummyItems: List<ItemModel> = listOf(
ItemModel(id = 1, title = "first item"),
ItemModel(id = 2, title = "second item"),
ItemModel(id = 3, title = "third item")
)
override fun getItems(order: OrderType): Flow<List<ItemModel>> = flow {
delay(timeMillis = 1000) // simulate network call delay
when (order) {
OrderType.ASCENDING -> {
emit(value = dummyItems)
}
OrderType.DESCENDING -> {
emit(value = dummyItems.sortedByDescending { it.id })
}
}
}
}
#HiltViewModel
class HomeViewModel #Inject constructor(
private val itemsRepository: ItemsRepository,
private val preferencesRepository: PreferencesRepository
) : ViewModel() {
private val _filter = MutableLiveData<OrderType>()
val filter: LiveData<OrderType> get() = _filter
private val _items = MutableLiveData<List<ItemModel>>()
val items: LiveData<List<ItemModel>> get() = _items
init {
// i imagine that these two methods must be unified somehow to work
getFilter()
getItems()
}
private fun getFilter() = viewModelScope.launch {
preferencesRepository.getFilter().collectLatest { orderType ->
_filter.value = orderType
println(orderType.name)
}
}
private fun getItems() = viewModelScope.launch {
itemsRepository.getItems(
order = _filter.value ?: OrderType.ASCENDING // not updated as expected
).collectLatest { items ->
_items.value = items
items.forEach { println(it.title) }
}
}
}
Logcat of current code:
You can use either flatMapLatest() or alternatively flatMapConcat() operator:
preferencesRepository.getFilter()
.flatMapLatest { itemsRepository.getItems(it) }
Whenever there is a new order emitted, it invokes getItems() with this order and then emits items from getItems().
flatMapLatest() only cares about the last order emitted. If new order is emitted before items for the last one are acquired, it just ignores the previous order(s) and cancels fetching of items for it (or don't even start fetching). It seems like this is what you need.
flatMapConcat() always invokes getItems() for each new order and waits for items before processing the next ordering.
Also, if this is your real case and not a simplified example for StackOverflow, then I suggest to not re-fetch items when ordering changes. You can re-order locally.

App slow after making a request inside another request

I am making a request with coroutines based on a user name, which returns a list of Object<Profile>, and with that list I am making another request with each object, and then switching and passing the info to another screen, but such process is making the app super slow and I would like to find a better way or a way to not making this process so slow. Here my code
Fragment from where I am starting the process and where the app is getting super slow
emptyHomeViewModel.getPlayersListByName(text)
emptyHomeViewModel.listOfPlayersByNameLiveData.observe(viewLifecycleOwner) { playersByName ->
emptyHomeViewModel.getPlayersProfileByName(playersByName)
emptyHomeViewModel.listOfProfilesByID.observe(viewLifecycleOwner) { profiles ->
if (profiles != null) {
val list: Array<Profile> = profiles.toTypedArray()
bundle = Bundle().apply {
putSerializable("user", list)
}
findNavController().navigate(
R.id.action_emptyHomeFragment_to_selectUserFragment,
bundle
)
}
}
}
ViewModel from where I am executing the coroutines and making the request to the API
fun getPlayersListByName(playerName: String) = viewModelScope.launch {
val playersList = getPlayersByPersonaNameUseCase.getPlayersByName(playerName)
if (playersList != null) {
_listOfPlayersByNameLiveData.postValue(playersList)
}
}
fun getPlayersProfileByName(playersByName: List<PlayerByPersonaNameItem>?) =
viewModelScope.launch {
var playersProfileList: ArrayList<Profile> = arrayListOf()
if (playersByName != null) {
for (player in playersByName) {
getPlayerByIDUseCase.getPlayerById(player.accountId)
?.let { playersProfileList.add(it) }
}
_listOfProfilesByID.postValue(playersProfileList)
}
}
You can actually load profiles in parallel, preventing loading them one after another, to decrease time of loading data:
fun getPlayersProfileByName(playersByName: List<PlayerByPersonaNameItem>?) =
viewModelScope.launch {
val playersProfileList: List<Profile> = playersByName?.map { player ->
async {
getPlayerByIDUseCase.getPlayerById(player.accountId)
}
}.awaitAll().filterNotNull()
_listOfProfilesByID.postValue(playersProfileList)
}
Also you can improve it a little bit by removing additional LiveData observer and calling getPlayersProfileByName right after you get playersList:
fun getPlayersListByName(playerName: String) = viewModelScope.launch {
val playersList = getPlayersByPersonaNameUseCase.getPlayersByName(playerName)
getPlayersProfileByName(playersList)
}

Proper way of handle sealed class property in kotlin

Hey I am working in Android Kotlin. I am learning this LatestNewsUiState sealed class example from Android doc. I made my own sealed class example. But I am confused little bit, how can I achieved this. Is I am doing right for my scenario or not?
DataState.kt
sealed class DataState {
data class DataFetch(val data: List<Xyz>?) : DataState()
object EmptyOnFetch : DataState()
object ErrorOnFetch : DataState()
}
viewmodel.kt
var dataMutableStateFlow = MutableStateFlow<DataState>(DataState.EmptyOnFetch)
fun fetchData() {
viewModelScope.launch {
val result = repository.getData()
result.handleResult(
onSuccess = { response ->
if (response?.items.isNullOrEmpty()) {
dataMutableStateFlow.value = DataState.EmptyOnFetch
} else {
dataMutableStateFlow.value = DataState.DataFetch(response?.items)
}
},
onError = {
dataMutableStateFlow.value = DataState.ErrorOnFetch
}
)
}
}
fun fetchMoreData() {
viewModelScope.launch {
val result = repository.getData()
result.handleResult(
onSuccess = { response ->
if (response?.items.isNullOrEmpty()) {
dataMutableStateFlow.value = DataState.EmptyOnFetch
} else {
dataMutableStateFlow.value = DataState.DataFetch(response?.items)
}
},
onError = {
dataMutableStateFlow.value = DataState.ErrorOnFetch
}
)
}
}
Activity.kt
lifecycleScope.launchWhenStarted {
viewModel.dataMutableStateFlow.collectLatest { state ->
when (state) {
is DataState.DataFetch -> {
binding.group.visibility = View.VISIBLE
}
DataState.EmptyOnFetch,
DataState.ErrorOnFetch -> {
binding.group.visibility = View.GONE
}
}
}
}
}
I have some points which I want to achieve in the standard ways.
1. When your first initial api call fetchData() if data is not null or empty then we need to show view. If data is empty or null then we need to hide the view. But if api fail then we need to show an error message.
2. When view is visible and view is showing some data. Then we call another api fetchMoreData() and data is empty or null then I don't want to hide view as per code is written above. And If api fails then we show error message.
Thanks in advance

Firestore startAt skips the snapshot given to it and behaves as startAfter instead

I have a PagingSource that pages through a firestore collection to return documents.
class ClipPageDataSource(mParams:Bundle, private val mAds:Boolean):PagingSource<QuerySnapshot, Clip>(), ClipDataSource {
var query : Query?= null
private val mFirestore = FirebaseFirestore.getInstance()
private var mBaseQuery = mFirestore.collection(SharedConstants.COLLECTION_CLIPS)
.orderBy("createdAt",Query.Direction.DESCENDING)
private var mLikedQuery = mFirestore.collection(SharedConstants.COLLECTION_USERS)
.document(Prefs.getString(SharedConstants.PREF_SERVER_USER_ID,Firebase().getCurrentUserId()))
.collection(SharedConstants.SUB_COLLECTION_USER_LIKES)
.orderBy("createdAt",Query.Direction.DESCENDING)
private var mSavedQuery = mFirestore.collection(SharedConstants.COLLECTION_USERS)
.document(Prefs.getString(SharedConstants.PREF_SERVER_USER_ID,Firebase().getCurrentUserId()))
.collection(SharedConstants.SUB_COLLECTION_SAVES)
.orderBy("createdAt",Query.Direction.DESCENDING)
val mine = mParams.getBoolean(ClipDataSource.PARAM_MINE)
val liked = mParams.getBoolean(ClipDataSource.PARAM_LIKED)
val saved = mParams.getBoolean(ClipDataSource.PARAM_SAVED)
val user = mParams.getString(ClipDataSource.PARAM_USER)
val first = mParams.getString(ClipDataSource.PARAM_FIRST)
val private = mParams.getBoolean(ClipDataSource.PARAM_PRIVATE)
override fun getRefreshKey(state: PagingState<QuerySnapshot, Clip>): QuerySnapshot? {
return null
}
override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Clip> {
try {
query = when {
liked -> {
mLikedQuery
}
saved -> {
mSavedQuery
}
else -> {
mBaseQuery
}
}
if(mine){
query = query!!.whereEqualTo("createdBy.uid",Prefs.getString(SharedConstants.PREF_SERVER_USER_ID,Firebase().getCurrentUserId()))
}else if(user!=null){
query = query!!.whereEqualTo("createdBy.uid",user)
}
query = if (private){
query!!.whereEqualTo("private",true)
}else{
query!!.whereEqualTo("private",false)
}
first?.let {
val item = mFirestore.collection(SharedConstants.COLLECTION_CLIPS).document(it).get().await()
if (item!=null){
query = query!!.startAt(item)
}
Log.d(TAG,"the first item fetched is ${item.data!!["id"]}")
}
query = query!!.limit(15)
val currentPage = params.key ?: query!!.get().await()
if (currentPage.size() < 1)
return LoadResult.Page(emptyList(),null,null)
val lastDocumentSnapshot = currentPage.documents[currentPage.size() - 1]
val nextPage = query!!.startAfter(lastDocumentSnapshot).get().await()
val clips = currentPage.map {
it.toObject(Clip::class.java)
}
return LoadResult.Page(clips,null,nextPage)
}catch (e:Exception){
return LoadResult.Error(e)
}
}
companion object{
private const val TAG = "DataSource"
}
}
So, in the above code, I have three different collections to fetch data from, and the required one is selected based on the parameters passed.
Now, when I fetch data using the mBaseQuery, and passing an id in first parameter, it returns the data correctly.
But, when I fetch data using the mLikedQuery or the mSavedQuery, instead of returning data from the id passed in first parameter, it uses the next item as the first one. Basically, startAt works as startAfter.
I have checked the snapshot fetched using the id passed in first is correct. So, the block in first?.let, works correctly. But, when the final query is executed, it skips the first item passed in startAt and instead starts from the next item in list.
This only happens with mLikedQuery and mSavedQuery and not with mBaseQuery.
Anybody got any idea what's happening here?
The DocumentReference you are providing to startAt is always for a document from the collection SharedConstants.COLLECTION_CLIPS:
val item = mFirestore.collection(SharedConstants.COLLECTION_CLIPS).document(it).get().await()
This works fine for your mBaseQuery because that query is querying the documents in the SharedConstants.COLLECTION_CLIPS collection, however your mLikedQuery and mSavedQuery are querying documents from different collections so providing a DocumentReference from the SharedConstants.COLLECTION_CLIPS collection as the startAt value here doesn't make sense, the query can't start at a document that doesn't exist in the collection you're querying.
Perhaps you need to set the item you provide to startAt based on which query is being used, e.g.:
...
first?.let {
val item = when {
liked -> {
mFirestore.collection(SharedConstants.COLLECTION_USERS)
.document(Prefs.getString(SharedConstants.PREF_SERVER_USER_ID,Firebase().getCurrentUserId()))
.collection(SharedConstants.SUB_COLLECTION_USER_LIKES).document(it).get().await()
}
saved -> {
mFirestore.collection(SharedConstants.COLLECTION_USERS)
.document(Prefs.getString(SharedConstants.PREF_SERVER_USER_ID,Firebase().getCurrentUserId()))
.collection(SharedConstants.SUB_COLLECTION_SAVES).document(it).get().await()
}
else -> {
mFirestore.collection(SharedConstants.COLLECTION_CLIPS).document(it).get().await()
}
}
...

Categories

Resources