I have the below function which works with Kotlin flow
sealed class State<out T> {
data class Loading<out T>(val progress: Int = 0) : State<T>()
data class Success<out T>(val data: T?) : State<T>()
data class Error(val exception: Throwable) : State<Nothing>()
}
suspend fun getReviews(movieId: Long): Flow<State<List<Review>>> = withContext(IO) {
flow { emit(api.getReviews(movieId)) }
.map { it.sortedWith { item, _ -> if (item.userId == pref.getUserId()) -1 else 0 } }
.map { State.Success(it) }
.catch { error -> emit(State.Error(error)).also { Timber.e(error) } }
}
I'm unable to emit an error state from the catch block when there is an exception. It gives me an error stating Type mismatch: inferred type is State.Error but State.Success<List<Review>> was expected
Why can't I emit the sub class of the State from the catch block?
Related
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 sealed class:
sealed class Resource<out T> {
object Loading: Resource<Nothing>()
data class Success<out T>(val data: T): Resource<T>()
data class Failure(val message: String): Resource<Nothing>()
}
In the repository class I have this function that deletes an item from an API:
override suspend fun deleteItem(id: String) = flow {
try {
emit(Resource.Loading)
emit(Resource.Success(itemsRef.document(id).delete().await()))
} catch (e: Exception) {
emit(Resource.Failure(e.message))
}
}
The result of the delete operation is Void?. Now, in the ViewModel class I declare:
val state = mutableStateOf<Resource<Void?>>(Success(null))
And update it when the delete completes:
fun deleteItem(id: String) {
viewModelScope.launch {
repo.deleteItem(id).collect { response ->
state.value = response
}
}
}
I have created a Card and inside onClick I have added:
IconButton(
onClick = viewModel.deleteItem(id),
)
Which actually deletes that item form database correctly. But I cannot track the result of the operation. I tried using:
when(val res = viewModel.state.value) {
is Resource.Loading -> Log.d(TAG, "Loading")
is Resource.Success -> Log.d(TAG, "Success")
is Resource.Failure -> Log.d(TAG, "Failure")
}
But only the case Loading is triggered. No success/failure at all. What can be wrong here? As it really acts like a synchronous operation.
I've tested your approach without a repository, and compose part looks totally fine:
var i = 0
#Composable
fun TestScreen(viewModel: TestViewModel = viewModel()) {
val state by viewModel.state
Text(
when (val stateSmartCast = state) {
is Resource.Failure -> "Failure ${stateSmartCast.message}"
Resource.Loading -> "Loading"
is Resource.Success -> "Success ${stateSmartCast.data}"
}
)
Button(onClick = {
viewModel.deleteItem(++i)
}) {
}
}
class TestViewModel : ViewModel() {
val state = mutableStateOf<Resource<Int>>(Resource.Success(i))
fun deleteItem(id: Int) {
viewModelScope.launch {
deleteItemInternal(id).collect { response ->
state.value = response
}
}
}
suspend fun deleteItemInternal(id: Int) = flow {
try {
emit(Resource.Loading)
delay(1000)
if (id % 3 == 0) {
throw IllegalStateException("error on third")
}
emit(Resource.Success(id))
} catch (e: Exception) {
emit(Resource.Failure(e.message ?: e.toString()))
}
}
}
So the the problem looks like in this line itemsRef.document(id).delete().await()), or in your connection to the repository.
Try collecting in the composable function:
val state = viewModel.state.collectAsState()
Then you can do: when (val res = viewModel.state.value){...}.
However I am sceptical about the deleteItem in the repository returning a flow. Do you really need such thing? You can always map stuff in the viewModel.
I have a lot of methods that look like that:
override suspend fun getBalance(): Result<BigDecimal> = withContext(Dispatchers.IO) {
Log.d(TAG, "Fetching balance from data store")
val balance = balancePreferencesFlow.firstOrNull()
?: return#withContext Result.Error(CacheIsInvalidException)
return#withContext when (balance) {
is Result.Success -> {
if ((balance.data.timestamp + ttl) <= getCurrentTime()) {
deleteBalance()
Result.Error(CacheIsInvalidException)
} else {
resultOf { balance.data.toDomainType() }
}
}
is Result.Error -> balance
}
}
There I am collecting a Flow of some type from DataStore, then if it is a Success Result(with data parameter of type T), I should get its timestamp(it is a data class field), and if the condition is true delete invalid data and if it's false return the converted Result.
The convertion functions look somehow like that:
fun BigDecimal.toPersistenceType(): Balance = Balance(
balanceAmount = this,
timestamp = getCurrentTime()
)
fun Balance.toDomainType(): BigDecimal = this.balanceAmount
I've tried to make an abstract method in this way, but I don't completely understand how I should pass a lambda to it.
suspend inline fun <reified T : Any, reified V : Any> getPreferencesDataStoreCache(
preferencesFlow: Flow<Result<V>>,
ttl: Long,
deleteCachedData: () -> Unit,
getTimestamp: () -> Long,
convertData: () -> T
): Result<T> {
val preferencesResult = preferencesFlow.firstOrNull()
return when (preferencesResult) {
is Result.Success -> {
if ((getTimestamp() + ttl) <= getCurrentTime()) {
deleteCachedData()
Result.Error(CacheIsInvalidException)
} else {
resultOf { preferencesResult.data.convertData() }
}
}
is Result.Error -> preferencesResult
else -> Result.Error(CacheIsInvalidException)
}
}
And a lambda for convertion should look like an extension method.
The Result class:
sealed class Result<out T : Any> {
data class Success<out Type : Any>(val data: Type) : Result<Type>()
data class Error(val exception: Exception) : Result<Nothing>()
}
First of all, I see here some cache work, that from my point should be placed in one interface.
interface Cache {
val timestamp: Long
fun clear()
}
You can make timestamp property nullable to return null if your cache is still empty - it's up to you.
Then universal method you need I assume to place inside Result class as it seems to be only its own work.
sealed class Result<out T : Any> {
data class Success<out Type : Any>(val data: Type) : Result<Type>()
data class Error(val exception: Exception) : Result<Nothing>()
fun <R : Any> convertIfValid(cache: Cache, ttl: Long, converter: (T) -> R) : Result<R> =
when (this) {
is Success -> {
if (cache.timestamp + ttl <= getCurrentTime()) {
cache.clear()
Error(CacheIsInvalidException())
} else {
Success(converter(data))
}
}
is Error -> this
}
}
May be it would be better to place getCurrentTime method in some injected entity too, but it's not important in this post.
By the way, as you can see here in when I didn't place else state as it is unnecessary for sealed classes.
From your code I can make an example of cache implementation only for balance:
class BalanceCache : Cache {
var balanceValue = Balance()
override val timestamp: Long
get() = balanceValue.timestamp
override fun clear() {
deleteBalance()
}
}
If you need more examples from me, please give me more details about your code where you want to use it.
I'm trying to understand Flow but few thing are not clear to me. I have a simple interface :
interface Operation<T> {
fun performAsync(callback: (T? , Throwable?) -> Unit)
fun cancel()
}
then I have a manager class with function :
fun<T : Any> Operation<T>.perform(): Flow<T> =
callbackFlow {
performAsync {
value , exception ->
when {
exception !=null -> close(exception) //operation had failed
value == null -> close() //operation had succeeded
else -> offer(value as T)
}
}
awaitClose { cancel() }
}
let's say I have a very simple operation - trying do use gson to serialize object to JSON :
fun convert () {
try {
val carJSON = gson.toJson(carObj)
//send Car value
} catch (e : Exception) {
//here I want to send exception and receive it in callback in activity/fragment.
}
}
Can you explain me, please, how to observe Exception (or T value) and send/receive it in Activity/Fragment ?
How about you use a sealed class.
sealed class Operation<out V, out E> {
data class Result<out V> (val data: V) : Operation<V, Nothing>()
data class Error<out E> (val error: E) : Operation<Nothing, E>()
companion object {
inline fun <V> build(operation : () -> V) : Response<V, Exception> {
return try {
Value(operation.invoke())
} catch (e: Exception) {
Error(e)
}
}
}
Then when you need response from some callback function as flow do the following.
fun someFunction() : Operation<Flow<String>, Exception> {
val flow = callbackFlow<String> {
val callback = object : someCallback {
onResult(result: String) {
offer(result)
}
onError(e: Exception) {
throw e
}
}
awaitClose { callback.remove() } //Just an example of a callback
}
return Operation.build { flow }
}
Now in Activity,
Simply get the result within some method like,
when(someFunction) {
is Operation.Result -> collect(someFunction().data)
is Operation.Error -> handleErrorAsHoweverYouWant(someFunction().error)
}
I've created a class like this:
sealed class Either<out L, out R> {
//Failure
data class Left<out L>(val value: L) : Either<L, Nothing>()
//Success
data class Right<out R>(val value: R) : Either<Nothing, R>()
val isRight get() = this is Right<R>
val isLeft get() = this is Left<L>
}
I am using retrofit and I am planning to return this:
#GET(__SOMEPATH__)
suspend fun pews(__Whatever__) : Either<Throwable, POJO>
But when Gson tries to create the object, an exception is thrown:
java.lang.RuntimeException: Failed to invoke private com.pew.pew.base.networking.Either() with no args
And also
Caused by: java.lang.InstantiationException: Can't instantiate abstract class com.pew.pew.base.networking.Either
Is there any way to encapsulate Error and Result on retrofit response?
EDIT
Now I have another sealed class
sealed class Result<T> {
data class Success<T>(val data: T) : Result<T>()
data class Unauthorized(val exception: Exception) : Result<Nothing>()
data class Timeout(val exception: Exception) : Result<Nothing>()
data class Error(val exception: Exception) : Result<Nothing>()
}
fun <A, B> Result<A>.map(mapper: (A) -> B): Result<out B> {
return when (this) {
is Success -> Success(mapper(data))
is Unauthorized -> Unauthorized(exception)
is Timeout -> Timeout(exception)
is Error -> Error(exception)
}
}
And then in my RepositoryImpl I need to define wether it is a Success or an Error. How do I do that? Before I was using a fold, which allowed me to get the Success or Error.
Can I do something like change from Call<T> to Result<T>?
inline fun <reified T> execute(f: () -> Call<T>): ResultWrapper<T> =
try {
when (T::class) {
Unit::class -> f().execute().let {
ResultWrapper.Success(Unit as T)
}
else -> f().execute().body()?.let {
ResultWrapper.Success(it)
} ?: ResultWrapper.Error(Throwable())
}
} catch (exception: Exception) {
ResultWrapper.Network(serverError)
}
But it says