I have emit exception inside flow and got below exception.
IllegalStateException: Flow exception transparency is violated:
Previous 'emit' call has thrown exception java.lang.NullPointerException, but then emission attempt of value 'planetbeyond.domain.api.Resource$Error#85b4d28' has been detected.
Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
For a more detailed explanation, please refer to Flow documentation.
at kotlinx.coroutines.flow.internal.SafeCollector.exceptionTransparencyViolated(SafeCollector.kt:140)
at kotlinx.coroutines.flow.internal.SafeCollector.checkContext(SafeCollector.kt:104)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:83)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
at planetbeyond.domain.use_cases.OptionSelectedCountUsecase$invoke$1.invokeSuspend(OptionSelectedCountUsecase.kt:20)
OptionSelectedCountUsecase.kt
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
emit(Resource.Success(data))
} catch (e: Exception) {
emit(Resource.Error(e.toString()))// crashed at this line when api don't response anything or some sort of server error
}
}
}
Repository.kt
interface Repository{
suspend fun getOptionSelectedCount(questionId: Int):List<OptionSelectedCountModel>
}
RepositoryImpl.kt
class RepositoryImpl #Inject constructor(
private val apiService: ApiService
) : Repository {
override suspend fun getOptionSelectedCount(questionId: Int): List<OptionSelectedCountModel> {
return apiService.getOptionSelectedCount(questionId).data.map {
it.toModel()
}
}
}
ApiService.kt
interface ApiService {
#GET("get_option_selected_count")
suspend fun getOptionSelectedCount(
#Query("question_id") question_id: Int
): WebResponse<List<OptionSelectedCountDto>>
}
LiveShowQuestionViewModel.kt
#HiltViewModel
class LiveShowQuestionsViewModel #Inject constructor(
private val optionSelectedCountUsecase: OptionSelectedCountUsecase
) : ViewModel() {
fun getOptionSelectedCount(questionId: Int) {
optionSelectedCountUsecase(questionId).onEach {
when (it) {
is Resource.Loading -> {
_optionSelectedCountState.value = OptionSelectedCountState(isLoading = true)
}
is Resource.Error -> {
_optionSelectedCountState.value = OptionSelectedCountState(error = it.message)
}
is Resource.Success -> {
_optionSelectedCountState.value = OptionSelectedCountState(data = it.data)
}
}
}///.catch { } // Why must I have to handle it here
.launchIn(viewModelScope)
}
}
Is it neccessary to handle exception outside flow like commented above. What is the best practice.
The problem is that you wrapped an emit call in try and try to emit in the matching catch block. This means that if the emit call itself throws (which ambiguously could be caused by some downstream problem with the flow) it's being instructing to emit again. This is very ambiguous and fragile behavior.
Instead, you can move your emit call(s) outside the try/catch:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
val result = try {
val data = repository.getOptionSelectedCount(questionId)
Resource.Success(data)
} catch (e: Exception) {
Resource.Error(e.toString())
}
emit(result)
}
}
Somehow, you're causing a NullPointerException in your collector. That's a separate problem to solve.
The root problem is that your
emit(Resource.Success(data))
throws an exception. When you catch that exception you are still in the "emit" block and you are trying to
emit(Resource.Error(e.toString())
So it's like emit inside emit. So yes this is wrong.
But let's get a step backward. Why there is an exception during the first emit? It looks like this data object is not properly filled with data, probably because of the issues that you mentioned (bad response etc), after it reaches the collector there is null pointer exception.
So basic flow should be
try to make the call, and catch http/parsing exception if there is one ( emit failure)
If there was no exception, validate if the object contains proper fields. If data is inconsistent emit Error
If everything is ok emit success
for example:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
emit(Resource.Success(data))
}else{
// some data integrity issues, missing fields
emit(Resource.Error("TODO error")
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
emit(Resource.Error(e.toString()))
}
}
}
This ideally should be split into, to not mess with exception catching of emit:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
emit(getResult(questionId))
}
fun getResult(questionId: Int): Resource<List<OptionSelectedCountModel>>{
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
return Resource.Success(data)
}else{
// some data integrity issues, missing fields
return Resource.Error("TODO error"
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
return Resource.Error(e.toString())
}
}
}
You should not emit exceptions and errors manually. Otherwise the user of the flow will not know, if exception actually happened, without checking the emitted value for being an error.
You want to provide exception transparency, therefore it is better to process them on collecting the flow.
One of the ways is to use catch operator. To simplify flow collecting we will wrap the catching behavior in a function.
fun <T> Flow<T>.handleErrors(): Flow<T> =
catch { e -> showErrorMessage(e) }
Then, while collecting the flow:
optionSelectedCountUsecase(questionId)
.onEach { ... }
.handleErrors()
.launchIn(viewModelScope)
Note, that if you want to process only the errors from invocation of the use case, you can change the order of operators. The previous order allows you to process errors from onEach block too. Example below will only process errors from use case invocation.
optionSelectedCountUsecase(questionId)
.handleErrors()
.onEach { ... }
.launchIn(viewModelScope)
Read more about exception handling in flows
Related
I have the below code in my view model class.
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableStateFlow(0)
val marketState: LiveData<State<Market>> =
retry.flatMapLatest{repo.refreshMarket()}
.map { State.Success(it) as State<T> }
.catch { error -> emit(State.Error(error)) }
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.value++
}
}
MarketRepository.kt:
fun refreshMarket() =
flow { emit(api.getMarkets()) }
.onEach { db.upsert(it) }
.flowOn(dispatchers.IO)
It works fine until a network error occurs in the repository method refreshMarket then when I call the retry() on the view model, it doesn't trigger the flatMapLatest transformer function anymore on the retry MutableStateFlow, why?
Does the flow get complete when it calls a Catch block? how to handle such situation?
You're right, catch won't continue emitting after an exception is caught. As the documentation says, it is conceptually similar to wrapping all the code above it in try. If there is a loop in a traditional try block, it does not continue iterating once something is thrown, for example:
try {
for (i in 1..10) {
if (i == 2) throw RuntimeException()
println(i)
}
} catch (e: RuntimeException) {
println("Error!")
}
In this example, once 2 is encountered, the exception is caught, but code flow does not return to the loop in the try block. You will not see any numbers printed that come after 2.
You can use retryWhen instead of catch to be able to restart the flow. To do it on demand like you want, maybe this strategy could be used (I didn't test it):
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableSharedFlow<Unit>()
val marketState: LiveData<State<Market>> =
repo.refreshMarket()
.map { State.Success(it) as State<T> }
.retryWhen { error, _ ->
emit(State.Error(error))
retry.first() // await next value from retry flow
true
}
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.tryEmit(Unit)
}
}
I am reading this article to fully understand the dos and donts of using Flow while comparing it to my implementation, but I can't grasp clearly how to tell if you are wasting resource when using Flow or flow builder. When is the time a flow is being release/freed in memory and when is the time that you are wasting resource like accidentally creating multiple instances of flow and not releasing them?
I have a UseCase class that invokes a repository function that returns Flow. In my ViewModel this is how it looks like.
class AssetViewModel constructor(private val getAssetsUseCase: GetAssetsUseCase) : BaseViewModel() {
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
}
The idea here is we are fetching data from remote every 10 seconds while also allowing on demand fetch of data via UI.
Just a typical useless UseCase class
class GetAssetsUseCase #Inject constructor(
private val repository: AssetsRepository // Passing interface not implementation for fake test
) {
operator fun invoke(baseUrl: String, query: String, limit: String): Flow<RequestStatus<AssetDomain>> {
return repository.fetchAssets(baseUrl, query, limit)
}
}
The concrete implementation of repository
class AssetsRepositoryImpl constructor(
private val service: CryptoService,
private val mapper: AssetDtoMapper
) : AssetsRepository {
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
) = flow {
try {
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
}
}
After reading this article which says that using stateIn or sharedIn will improve the performance when using a flow, it seems that I am creating new instances of the same flow on-demand. But there is a limitation as the stated approach only works for variable and not function that returns Flow.
stateIn and shareIn can save resources if there are multiple observers, by avoiding redundant fetching. And in your case, you could set it up to automatically pause the automatic re-fetching when there are no observers. If, on the UI side you use repeatOnLifecycle, then it will automatically drop your observers when the view is off screen and then you will avoid wasted fetches the user will never see.
I think it’s not often described this way, but often the multiple observers are just observers coming from the same Activity or Fragment class after screen rotations or rapidly switching between fragments. If you use WhileSubscribed with a timeout to account for this, you can avoid having to restart your flow if it’s needed again quickly.
Currently you emit to from an external coroutine instead of using shareIn, so there’s no opportunity to pause execution.
I haven't tried to create something that supports both automatic and manual refetching. Here's a possible strategy, but I haven't tested it.
private val refreshRequest = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets() {
refreshRequest.trySend(Unit)
}
val assetState = flow {
while(true) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).map {
when(it){
is RequestStatus.Loading -> AssetState.FetchLoading
is RequestStatus.Success -> AssetState.FetchSuccess(it.data.assetDataDomain)
is RequestStatus.Failed -> AssetState.FetchFailed(it.message)
}
}.emitAll()
withTimeoutOrNull(100L) {
// drop any immediate or pending manual request
refreshRequest.receive()
}
// Wait until a fetch is manually requested or ten seconds pass:
withTimeoutOrNull(10000L - 100L) {
refreshRequest.receive()
}
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(4000L), replay = 1)
To this I would recommend not using flow as the return type of the usecase function and the api call must not be wrapped inside a flow builder.
Why:
The api call actually is happening once and then again after an interval it is triggered by the view model itself, returning flow from the api caller function will be a bad usage of powerful tool that is actually meant to be called once and then it must be self-reliant, it should emit or pump in the data till the moment it has a subscriber/collector.
One usecase you can consider when using flow as return type from the room db query call, it is called only once and then the room emits data into it till the time it has subscriber.
.....
fun fetchAssets() {
viewModelScope.launch {
// loading true
val result=getusecase(.....)
when(result){..process result and emit on state..}
// loading false
}
}
.....
suspend operator fun invoke(....):RequestStatus<AssetDomain>{
repository.fetchAssets(baseUrl, query, limit)
}
.....
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
):RequestStatus {
try {
//RequestStatus.Loading()//this can be managed in viewmodel itself
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
RequestStatus.Success(domainModel)
} catch (e: HttpException) {
RequestStatus.Failed(e)
} catch (e: IOException) {
RequestStatus.Failed(e)
}
}
I have an android app that I have built up an architecture similar to the Google IO App. I use the CoroutineUseCase from that app (but wrap results in a kotlin.Result<T> instead).
The main code looks like this:
suspend operator fun invoke(parameters: P): Result<R> {
return try {
withContext(Dispatchers.Default) {
work(parameters).let {
Result.success(it)
}
}
} catch (e: Throwable) {
Timber.e(e, "CoroutineUseCase Exception on ${Thread.currentThread().name}")
Result.failure<R>(e)
}
}
#Throws(RuntimeException::class)
protected abstract suspend fun work(parameters: P): R
Then in my view model I am invoking the use case like this:
viewModelScope.launch {
try {
createAccountsUseCase(CreateAccountParams(newUser, Constants.DEFAULT_SERVICE_DIRECTORY))
.onSuccess {
// Update UI for success
}
.onFailure {
_errorMessage.value = Event(it.message ?: "Error")
}
} catch (t: Throwable) {
Timber.e("Caught exception (${t.javaClass.simpleName}) in ViewModel: ${t.message}")
}
My problem is even though the withContext call in the use case is wrapped with a try/catch and returned as a Result, the exception is still thrown (hence why I have the catch in my view model code - which i don't want). I want to propagate the error as a Result.failure.
I have done a bit of reading. And my (obviously flawed) understanding is the withContext should create a new scope so any thrown exceptions inside that scope shouldn't cancel the parent scope (read here). And the parent scope doesn't appear to be cancelled as the exception caught in my view model is the same exception type thrown in work, not a CancellationException or is something unwrapping that?. Is that a correct understanding? If it isn't what would be the correct way to wrap the call to work so I can safely catch any exceptions and return them as a Result.failure to the view model.
Update:
The implementation of the use case that is failing. In my testing it is the UserPasswordInvalidException exception that is throwing.
override suspend fun work(parameters: CreateAccountParams): Account {
val tokenClient = with(parameters.serviceDirectory) {
TokenClient(tokenAuthorityUrl, clientId, clientSecret, moshi)
}
val response = tokenClient.requestResourceOwnerPassword(
parameters.newUser.emailAddress!!,
parameters.newUser.password!!,
"some scopes offline_access"
)
if (!response.isSuccess || response.token == null) {
response.statusCode?.let {
if (it == 400) {
throw UserPasswordInvalidException("Login failed. Username/password incorrect")
}
}
response.exception?.let {
throw it
}
throw ResourceOwnerPasswordException("requestResourceOwnerPassword() failed: (${response.message} (${response.statusCode})")
}
// logic to create account
return acc
}
}
class UserPasswordInvalidException(message: String) : Throwable(message)
class ResourceOwnerPasswordException(message: String) : Throwable(message)
data class CreateAccountParams(
val newUser: User,
val serviceDirectory: ServiceDirectory
)
Update #2:
I have logging in the full version here is the relevant details:
2020-09-24 18:12:28.596 25842-25842/com.ipfx.identity E/CoroutineUseCase: CoroutineUseCase Exception on main
com.ipfx.identity.domain.accounts.UserPasswordInvalidException: Login failed. Username/password incorrect
at com.ipfx.identity.domain.accounts.CreateAccountsUseCase.work(CreateAccountsUseCase.kt:34)
at com.ipfx.identity.domain.accounts.CreateAccountsUseCase.work(CreateAccountsUseCase.kt:14)
at com.ipfx.identity.domain.CoroutineUseCase$invoke$2.invokeSuspend(CoroutineUseCase.kt:21)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2020-09-24 18:12:28.598 25842-25842/com.ipfx.identity E/LoginViewModel$createAccount: Caught exception (UserPasswordInvalidException) in ViewModel: Login failed. Username/password incorrect
The full exception is logged inside the catching in CoroutineUseCase.invoke. And then again the details logged inside the catch in the view model.
Update #3
#RKS was correct. His comment caused me to look deeper. My understanding was correct on the exception handling. The problem was in using the kotlin.Result<T> return type. I am not sure why yet but I was somehow in my usage of the result trigger the throw. I switched the to the Result type from the Google IO App source and it works now. I guess enabling its use as a return type wasn't the smartest.
try/catch inside viewModelScope.launch {} is not required.
The following code is working fine,
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class TestCoroutines {
private suspend fun work(): String {
delay(1000)
throw Throwable("Exception From Work")
}
suspend fun invoke(): String {
return try {
withContext(Dispatchers.Default) {
work().let { "Success" }
}
} catch (e: Throwable) {
"Catch Inside:: invoke"
}
}
fun action() {
runBlocking {
val result = invoke()
println(result)
}
}
}
fun main() {
TestCoroutines().action()
}
Please check the entire flow if same exception is being thrown from other places.
Usually I'm returning from my dao suspend function:
#Dao
interface DataDao {
#Query("SELECT * FROM data")
fun getAllData(): List<Data>
}
And handle the call within the repository:
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val cacheResult = safeDatabaseCall(dispatcher = Dispatchers.IO) { dataDao.getAllData() }
//handle cacheResult, convert to DataState, emit DataState values
}.flowOn(Dispatchers.IO)
}
With generic fun:
suspend fun <T> safeDatabaseCall(
dispatcher: CoroutineDispatcher,
cacheCall: suspend () -> T?
): CacheResult<T?> {
return withContext(dispatcher) {
try {
withTimeout(10000L) {
CacheResult.Success(cacheCall.invoke())
}
} catch (t: Throwable) {
when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
}
}
}
The problem is that I want return fun getAllData(): Flow<List<Data>> instead of fun getAllData(): List<Data> In order to get immediate updates, But if I'm returning Flow from the Dao, I can't handle the call with safe call and catch errors.
I thought about collecting the data, but if i'm collecting the data the call already done without error handling
Basically I need the cache result return CacheResult<Data> and not CacheResult<Flow<Data>>
How can I solve the problem And make a generic safeDatabaseCall while returning Flow from Dao?
So if I understand correctly you just want to handle the query and return of information safely in a flow. My only question is around the types. I can sorta assume Data DataState and CacheResult are not the same types so I use a "magic" function that converts the intermediary values to the correct one. You will need to adjust accordingly
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val result = safeDatabaseCall(dispatcher = Dispatchers.IO) {
dataDao.getAllData()
}
// Emit the result
emit(result)
}.catch { t : Throwable ->
// Do our transformation like before
val result = when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
// And because catch is actually extending a FlowCollector
// We can emit the result in the stream
emit(result)
}.map { cacheResult ->
convertToDataOrDataState(cacheResult)
}
You shouldn't need flowOn with a dispatcher here since the work inside this flow doesn't require thread dispatching
to Dispatcher.IO. The code we are putting in our flow, is purely exception handling and invoking a function. The only place that seems to require any manual dispatch changing is, safeDatabaseCall(). I am not familiar with this function but if it does exist and takes a dispatcher for the result of actualing making the db calls on an IO thread, then all should be good without flowOn. Otherwise you will be switching dispatchers from original dispatcher -> IO and then to IO again. It's not much but the extra no-op context switch doesn't add anything other than confusion later on.
The flow itself traps any upstream issues and you then make them part of the resulting flow
I am building an app based off of the Android Clean Architecture Kotlin version (https://github.com/android10/Android-CleanArchitecture-Kotlin).
Using this architecture, each time you want to invoke a use case, a Kotlin coroutine is launched and the result is posted in the main thread. This is achieved by this code:
abstract class UseCase<out Type, in Params> where Type : Any {
abstract suspend fun run(params: Params): Either<Failure, Type>
fun execute(onResult: (Either<Failure, Type>) -> Unit, params: Params) {
val job = async(CommonPool) { run(params) }
launch(UI) { onResult.invoke(job.await()) }
}
In his example architecture, Mr. Android10 uses Retrofit to make a synchronous api call inside the kotlin couroutine. For example:
override fun movies(): Either<Failure, List<Movie>> {
return when (networkHandler.isConnected) {
true -> request(service.movies(), { it.map { it.toMovie() } }, emptyList())
false, null -> Left(NetworkConnection())
}
}
private fun <T, R> request(call: Call<T>, transform: (T) -> R, default: T): Either<Failure, R> {
return try {
val response = call.execute()
when (response.isSuccessful) {
true -> Right(transform((response.body() ?: default)))
false -> Left(ServerError())
}
} catch (exception: Throwable) {
Left(ServerError())
}
}
'Either' represents a disjoint type, meaning the result will either be a Failure or the object of type T you want.
His service.movies() method is implemented like so (using retrofit)
#GET(MOVIES) fun movies(): Call<List<MovieEntity>>
Now here is my question. I am replacing retrofit with Google Cloud Firestore. I know that currently, Firebase/Firestore is an all async library. I want to know if anyone knows of a method more elegant way of making a synchronous API call to Firebase.
I implemented my own version of Call:
interface Call<T: Any> {
fun execute(): Response<T>
data class Response<T>(var isSuccessful: Boolean, var body: T?, var failure: Failure?)
}
and my API call is implemented here
override fun movieList(): Call<List<MovieEntity>> = object : Call<List<MovieEntity>> {
override fun execute(): Call.Response<List<MovieEntity>> {
return movieListResponse()
}
}
private fun movieListResponse(): Call.Response<List<MovieEntity>> {
var response: Call.Response<List<MovieEntity>>? = null
FirebaseFirestore.getInstance().collection(DataConfig.databasePath + MOVIES_PATH).get().addOnCompleteListener { task ->
response = when {
!task.isSuccessful -> Call.Response(false, null, Failure.ServerError())
task.result.isEmpty -> Call.Response(false, null, MovieFailure.ListNotAvailable())
else -> Call.Response(true, task.result.mapTo(ArrayList()) { MovieEntity.fromSnapshot(it) }, null)
}
}
while (response == null)
Thread.sleep(50)
return response as Call.Response<List<MovieEntity>>
}
Of course, the while loop at the end bothers me. Is there any other, more elegant ways, to wait for the response to be assigned before returning from the movieListResponse method?
I tried calling await() on the Task that is returned from the Firebase get() method, but the movieListResponse method would return immediately anyway. Thanks for the help!
So I found what I was looking for in the Google Tasks API: "If your program is already executing in a background thread you can block a task to get the result synchronously and avoid callbacks" https://developers.google.com/android/guides/tasks#blocking
So my previous problematic code becomes:
private fun movieListResponse(): Call.Response<List<MovieEntity>> {
return try {
val taskResult = Tasks.await(FirebaseFirestore.getInstance().
collection(DataConfig.databasePath + MOVIES_PATH).get(), 2, TimeUnit.SECONDS)
Call.Response(true, taskResult.mapTo(ArrayList()) { MovieEntity.fromSnapshot(it) }, null)
} catch (e: ExecutionException) {
Call.Response(false, null, Failure.ServerError())
} catch (e: InterruptedException) {
Call.Response(false, null, Failure.InterruptedError())
} catch (e: TimeoutException) {
Call.Response(false, null, Failure.TimeoutError())
}
}
Note I no longer need my Thread.sleep while loop.
This code should only be run in a background thread/kotlin coroutine.
This is overengineered, there are several layers trying to do the same thing. I suggest you go back a few steps, undo the abstractions and get into the mood of using coroutines directly. Implement a suspend fun according to this template. You don't need the crutches of Either, handle exceptions in the most natural way: a try-catch around a suspend fun call.
You should end up with a signature as follows:
suspend fun movieList(): List<MovieEntity>
Call site:
launch(UI) {
try {
val list = movieList()
...
} catch (e: FireException) {
// handle
}
}
That's is not the way how firebase works. Firebase is based on callback.
I recommend architecture component's livedata.
Please check the following example.
here is a link: https://android.jlelse.eu/android-architecture-components-with-firebase-907b7699f6a0