INTRODUCTION:
I created an android project following this example: https://github.com/android/architecture-samples/
I had to add a class that holds the response status(Success/Error) and it's value, in the repository it looks basically like this:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
It is meant to be better than classic:
class Result<T> (
val success: Boolean,
val data: T?,
val exception: Exception?
)
Because :
- In this case the Success result definitely has only data and the Error only exception.
- As both Success and Error Message are inheriting from Result, due to Kotlin Smart Casts, the validation looks simpler:
var responce: Result<DataEntity> = dataSource.GetData()
if (responce is Success) {
doSomethingWith(responce.data)
} else if (responce is Error) {
throw responce.exception
}
PROBLEM:
All good, but when i'm trying to asynchronously observe data from a local dataSource (using Room lib):
interface TaskDao {
#Query("SELECT * FROM tasks")
fun observeTasks(): LiveData<List<TaskEntity>>
}
class SqlLocalDataSource(
private val taskDao: TaskDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
): LocalDataSource {
override suspend fun observeTasks(): LiveData<Result<List<TaskEntity>>> = withContext(ioDispatcher) {
taskDao.observeTasks().map {
Success(it)
}
}
}
It gives me the following Error: Type inference failed. Expected type mismatch: inferred type is LiveData<Result.Success<List<TaskEntity>>> but LiveData<Result<List<TaskEntity>>> was expected
Which is strange because Success inherits from Result
I TRYED:
I make sure that the types are correct (Result ids not from some other library)
To make the function run on the main thread, but its impossible to call DB from the main thread
It gives me the following Error: Type inference failed. Expected type mismatch: inferred type is LiveData<Result.Success<List<TaskEntity>>> but LiveData<Result<List<TaskEntity>>> was expected
Which is strange because Success inherits from Result
But LiveData<Success<...>> does not inherit from LiveData<Result<...>>. Please read about variance and take into account that LiveData is declared in Java and so can't be covariant.
I don't know why type inference would fail with suspend and work without it, but the problem can be fixed by being more explicit about types:
taskDao.observeTasks().map {
Success(it) as Result<List<TaskEntity>>
}
Or better, to avoid a cast:
fun <T> success(x: T): Result<T> = Success(x)
taskDao.observeTasks().map {
success(it)
}
The Problem was that its not permitted to Use LiveData in a suspend function, after I changed the function to a non suspended the error disappeared :
override fun observeTasks(): LiveData<Result<List<TaskEntity>>> {
return taskDao.observeTasks().map {
Success(it)
}
}
May be somebody can explain why is it?
Related
In order to handle Retrofit api calls I have a sealed interface as below:
sealed interface DataSourceResponseWrapper<out T> {
data class Success<out T>(val result: T) : DataSourceResponseWrapper<T>
data class Error(val throwable: Throwable) : DataSourceResponseWrapper<Nothing>
}
As you can see Success<out T> is a generic class but Error is not.
Here is ItemDataRepository:
class DataRepository constructor(private val itemApiDataSource: ItemApiDataSource) {
suspend fun loadData(search: String, days: Int, aqi: Boolean, alerts: Boolean): DataSourceResponseWrapper<LocalItemResponse> =
coroutineScope {
withContext(Dispatchers.IO) {
val response = try {
DataSourceResponseWrapper.Success(weatherApiDataSource.getWeatherInfo(search, days, if (aqi) "yes" else "no", if (alerts) "yes" else "no"))
} catch (throwable: Throwable) {
DataSourceResponseWrapper.Error(throwable)
}
when (response) {
is DataSourceResponseWrapper.Success<ApiWeatherResponse> ->
DataSourceResponseWrapper.Success(WeatherConverter.convertToLocal(response.result))
is DataSourceResponseWrapper.Error -> response
}
}
}
In DataRepository class I check if the result is successful or not. If it is, repository class converts the returned ApiItemResponse to a LocalItemResponse and returns the result. And if it's not, returns the response itself
The code works perfectly fine until I change when statements to the following:
when (response) {
is DataSourceResponseWrapper.Success<ApiWeatherResponse> ->
DataSourceResponseWrapper.Success(WeatherConverter.convertToLocal(response.result))
else -> response
}
It gives me an error saying:
Required:
DataSourceResponseWrapper<LocalItemResponse>
Found:
DataSourceResponseWrapper<Any>
So my question is Why Kotlin does not smartly cast the response as before? And the other question is How can I use else without needing to check type?
Some cases are simply too sophisticated (too many steps of logic) for the compiler to sort out and infer the types for you. In this case, instead of using an else branch, you can be specific. This is the way you should be using a sealed type anyway (no else branch if it can be avoided):
when (response) {
is DataSourceResponseWrapper.Success<ApiWeatherResponse> ->
DataSourceResponseWrapper.Success(WeatherConverter.convertToLocal(response.result))
is DataSourceResponseWrapper.Error -> response
}
You can in fact write
else -> response as DataSourceResponseWrapper<LocalItemResponse>
to make it work. Although the IDE is likely to warn for an unchecked cast. But it's just a warning. You can safely ignore it because you know for a fact that you can cast it there. It will compile and run just fine. But admittedly it is ugly to have the warning there.
I am working on an android Application and I opted to use Kotlin Result class so as to handle success/failure on my operations. I made the changes to the code, but the tests stop working and I cannot understand why. Here I show you some snippets:
FireStoreClient.kt
suspend fun items(): Result<ItemsResponse>
NetworkDataSource.kt
suspend fun getItems(): List<Item> =
fireStoreClient.items().fold({ it.items.map { item -> item.toDomain() } }, { emptyList() })
NetworkDataSourceTest.kt
#ExperimentalCoroutinesApi
#Test
fun `Check getItems works properly`() = runBlockingTest {
whenever(fireStoreClient.items()).doReturn(success(MOCK_ITEMS_DOCUMENT))
val expectedResult = listOf(
Item(
id = 1,
desc = "Description 1"
),
Item(
id = 2,
desc = "Description 2"
)
)
assertEquals(expectedResult, dataSource.getItems())
}
And this is the exception I am getting right now. Any clue? It appears that the fold() method is not being executed when unit testing.
java.lang.ClassCastException: kotlin.Result cannot be cast to ItemsResponse
at NetworkDataSource.getItems(NetworkDataSource.kt:31)
I've found a different workaround for this result-wrapping issue, for those who don't want to make their own Result type.
This issue appears to happens specifically when using Mockito's .thenReturn on suspend functions. I've found that using .thenAnswer doesn't exhibit the problem.
So instead of writing this in your unit test (changed doReturn to thenReturn here):
whenever(fireStoreClient.items()).thenReturn(success(MOCK_ITEMS_DOCUMENT))
Use:
whenever(fireStoreClient.items()).thenAnswer { success(MOCK_ITEMS_DOCUMENT) }
Edit: I should note that I was still experiencing this issue when running Kotlin 1.5.0.
Edit: On Kotlin 1.5.20 I can use .thenReturn again.
After a deep dive into the problem, finally, I've found a temporary workaround that works in the testing environment. The problem is, somehow the value of the Result object is wrapped by another Result, and we can pull the desired value or exception using reflection.
So, I've created an extension function called mockSafeFold, which implements the fold behavior in normal calls, and acts fine when you are executing unit-tests.
inline fun <R, reified T> Result<T>.mockSafeFold(
onSuccess: (value: T) -> R,
onFailure: (exception: Throwable) -> R
): R = when {
isSuccess -> {
val value = getOrNull()
try {
onSuccess(value as T)
} catch (e: ClassCastException) {
// This block of code is only executed in testing environment, when we are mocking a
// function that returns a `Result` object.
val valueNotNull = value!!
if ((value as Result<*>).isSuccess) {
valueNotNull::class.java.getDeclaredField("value").let {
it.isAccessible = true
it.get(value) as T
}.let(onSuccess)
} else {
valueNotNull::class.java.getDeclaredField("value").let {
it.isAccessible = true
it.get(value)
}.let { failure ->
failure!!::class.java.getDeclaredField("exception").let {
it.isAccessible = true
it.get(failure) as Exception
}
}.let(onFailure)
}
}
}
else -> onFailure(exceptionOrNull() ?: Exception())
}
Then, simply call it instead of fold:
val result: Result = myUseCase(param)
result.mockSafeFold(
onSuccess = { /* do whatever */ },
onFailure = { /* do whatever */ }
)
I had the same issue.
I noticed that my method of injected class which should return Result<List<Any>> returns actually Result<Result<List<Any>>> which causes the ClassCastException. I used the Evaluate Expression option for the result from the method and I got
Success(Success([]))
The app works well but unit tests didn't pass due this problem.
As a temporary solution I built a new simple implementation of Result sealed class with fold() extension function. It should be easy to replace in future to kotlin.Result
Result sealed class:
sealed class Result<T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure<T>(val error: Throwable) : Result<T>()
}
fold() extension function:
inline fun <R, T> Result<T>.fold(
onSuccess: (value: T) -> R,
onFailure: (exception: Throwable) -> R
): R = when (this) {
is Result.Success -> onSuccess(value)
is Result.Failure -> onFailure(error)
}
I have a sealed class which represents the Retrofit Response of my API.
sealed class NetworkResponse<out T : Any, out U : Any> {
data class Success<T : Any>(val body: T) : NetworkResponse<T, Nothing>()
data class ApiError<U : Any>(val body: U, val code: Int) : NetworkResponse<Nothing, U>()
data class NetworkError(val error: IOException) : NetworkResponse<Nothing, Nothing>()
data class UnknownError(val error: Throwable?) : NetworkResponse<Nothing, Nothing>()
}
So now i want to create a function that handles all the errors of a failed request. I want to have only one argument that represents either ApiError or NetworkError or UnknownError
fun networkErrorHanlder(mError: <what_should_i_put_here??>) {
// check if error is Api or Network or Unknown and do stuff...
}
What should be the type of the argument?
Since you care only about ApiError, NetworkError and UnknownError, which all derive from NetworkResponse but don't use the first generic type, you can specify that you don't care about it using * (Actually, depending on what you want to do with mError, you can replace U with * too - that is the case in the code below, but I introduced U just in case). In that case, you should accept a NetworkReponse:
fun <U : Any> networkErrorHanlder(mError: NetworkResponse<*, U>) {
when(mError) {
is NetworkResponse.ApiError ->
print("Api stuff: ${mError.body}")
is NetworkResponse.NetworkError ->
print ("Network stuff: ${mError.error}")
is NetworkResponse.UnknownError ->
print("Unknown: ${mError.error}")
else -> print("It must've been a Success...")
}
}
I've been struggling with MVVM pattern and android-architecture-components for last couple of months.
In my last project although I did try to decouple some of my app logic but it ended with a lot of code mess. Every fragment did lots of work, from handling UI to handling network requests and ...
In this new App I followed best practices for android app architecture and till now it's going well. But the thing is, I don't know how to handle network errors, and I don't get it how should I notify user if some network calls fail.
After searching and reading some blog posts I ended up with the following code (SafeApiCall and SafeApiResutl) functions to handle network requests in one place, but the thing is, All my Network Requests are done using retrofit and a NetworkDataSource class, Then I pass The NetworkDataSource and Dao to the RepositoryImpl class which is an implementation of my Repository Interface. then I pass the Repository to the viewModel, So ViewModel knows nothing about network or Dao or what so ever. So here is the problem, How can I notify user in case of any network errors ? I thought about creating a LiveData<> and pass errors to it in network layer, but in this case, Repository Must observe this, and also let's say create a LiveData in repository so viewModel observe that and so on ... But this is too much chaining, I dont like the idea of doing that. I also did take a look at the GoogleSamples Todo-MVVM-live-kotlin project, but honestly I didn't understand what is going on.
suspend fun <T : Any> safeApiCall(call: suspend () -> Response<BasicResponse<T>>, errorMessage: String): T? {
return when (val result = safeApiResult(call)) {
is NetworkResult.Success -> {
Timber.tag("safeApiCall").d("data is ${result.serverResponse.data}")
result.serverResponse.data
}
is NetworkResult.Error -> {
Timber.tag("SafeApiCall").e("$errorMessage & Exception - ${result.exception}")
null
}
else -> TODO()
}
}
private suspend fun <T : Any> safeApiResult(
call: suspend () -> Response<BasicResponse<T>>
): NetworkResult<T> {
return try {
val response = call.invoke()
Timber.tag("SafeApiResult")
.d("response code : ${response.code()}, server value : ${response.body()!!.status}, server message: ${response.body()!!.message}")
if (response.isSuccessful) {
return when (ServerResponseStatus.fromValue(response.body()!!.status)) {
ServerResponseStatus.SUCCESS -> NetworkResult.Success(response.body()!!)
ServerResponseStatus.FAILED -> TODO()
ServerResponseStatus.UNKNOWN -> TODO()
}
} else {
TODO()
}
} catch (exception: Exception) {
Timber.tag("SafeApiResultFailed").e(exception)
NetworkResult.Error(exception)
}
}
sealed class NetworkResult<out T : Any> {
data class Success<out T : Any>(val serverResponse: BasicResponse<out T>) : NetworkResult<T>()
data class Error(val exception: Exception) : NetworkResult<Nothing>()
}
You are in the correct path. I would place both methods in the NetworkDataSource. All the calls executed should call those methods to handle the errors.
The NetworkDataSource will return the NetworkResult to the repository, and it will return the result to the ViewModel.
As you say, you can use a LiveData to notify the Activity/Fragment. You can create an error data class:
data class ErrorDialog(
val title: String,
val message: String
)
And declare a LiveData<ErrorDialog> that will be observed from your view. Then when you receive notifications in your view, you can implement logic in a BaseActivity/BaseFragment to show a Dialog or Toast or whatever type of view to indicate the error.
I am encountering an issue that the following code can't be compiled in kotlin.
// StateModel.kt
sealed class StateModel
class Loading : StateModel()
data class Success<T: Any>(val data: T) : StateModel()
data class MyError(val message: String) : StateModel()
// StateModelTransformer.kt
class StateModelTransformer<T: Any> : FlowableTransformer<T, StateModel> {
override fun apply(upstream: Flowable<T>): Publisher<StateModel> {
return upstream
.map { data -> Success(data) }
.onErrorReturn { error ->
MyError(error.message) // compile error, Type mismatch, Require Success<T>, Found MyError
}
.startWith(Loading()) // compile error, none of the following function can be called with the arguments supplied
}
}
I have no idea why the onErrorReturn says requiring a Success<T> type but a StateModel type.
Thanks
Here are the relevant declarations in Flowable, for reference. Let's ignore onErrorReturn; it's not relevant to the problem here.
public Flowable<T> {
public <R> Flowable<R> map(Function<? super T, ? extends R> mapper);
public Flowable<T> startWith(T value);
}
These are the types Kotlin infers.
val upstream: Flowable<T>
val mapper: (T) -> Success<T> = { data -> Success(data) }
val map: ((T) -> Success<T>) -> Flowable<Success<T>>
= upstream::map
val mapped: Flowable<Success<T>> = map(mapper)
val loading: Loading = Loading()
val startWith:
(Success<T>) -> Flowable<Success<T>>
= mapped::startWith
startWith(loading) // type mismatch
The more specific Success<T> type was already inferred earlier, and Kotlin does not backtrack to find the more general StateModel type. To force that to happen, you can manually state the types, for example
// be explicit about the general type of the mapper
upstream.map { data -> Success(data) as StateModel }.startWith(Loading())
// be explicit about the generic type R = StateModel
upstream.map<StateModel> { data -> Success(data) }.startWith(Loading())
Incidentally, you're currently losing <T> in StateModel. I would suggest changing the base class to include the type parameter.
sealed class StateModel<out T: Any>
object Loading : StateModel<Nothing>()
data class Success<T: Any>(val data: T) : StateModel<T>()
data class MyError(val message: String) : StateModel<Nothing>()
This will let you write, for example,
val <T: Any> StateModel<T>.data: T?
get() = when (this) {
is Success -> data
else -> null
}