Simplify error handling

This commit is contained in:
Kirill Kamakin
2022-04-05 15:32:57 +05:00
parent 21feea145a
commit eca325ebe4
11 changed files with 61 additions and 52 deletions

View File

@@ -2,11 +2,12 @@ package gq.kirmanak.mealient.data.auth.impl
import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.network.ErrorDetail import gq.kirmanak.mealient.data.network.ErrorDetail
import gq.kirmanak.mealient.data.network.NetworkError.* import gq.kirmanak.mealient.data.network.NetworkError.NotMealie
import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull
import kotlinx.coroutines.CancellationException import gq.kirmanak.mealient.extensions.mapToNetworkError
import kotlinx.serialization.SerializationException import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
@@ -22,12 +23,7 @@ class AuthDataSourceImpl @Inject constructor(
override suspend fun authenticate(username: String, password: String): String { override suspend fun authenticate(username: String, password: String): String {
Timber.v("authenticate() called with: username = $username, password = $password") Timber.v("authenticate() called with: username = $username, password = $password")
val authService = try { val authService = authServiceFactory.provideService()
authServiceFactory.provideService()
} catch (e: Exception) {
Timber.e(e, "authenticate: can't create Retrofit service")
throw MalformedUrl(e)
}
val response = sendRequest(authService, username, password) val response = sendRequest(authService, username, password)
val accessToken = parseToken(response) val accessToken = parseToken(response)
Timber.v("authenticate() returned: $accessToken") Timber.v("authenticate() returned: $accessToken")
@@ -38,14 +34,11 @@ class AuthDataSourceImpl @Inject constructor(
authService: AuthService, authService: AuthService,
username: String, username: String,
password: String password: String
): Response<GetTokenResponse> = try { ): Response<GetTokenResponse> = runCatchingExceptCancel {
authService.getToken(username, password) authService.getToken(username, password)
} catch (e: Throwable) { }.getOrElse {
throw when (e) { Timber.e(it, "sendRequest: can't request token")
is CancellationException -> e throw it.mapToNetworkError()
is SerializationException -> NotMealie(e)
else -> NoServerConnection(e)
}
} }
private fun parseToken( private fun parseToken(

View File

@@ -1,10 +1,9 @@
package gq.kirmanak.mealient.data.baseurl package gq.kirmanak.mealient.data.baseurl
import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.mapToNetworkError
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.versionInfo import gq.kirmanak.mealient.extensions.versionInfo
import kotlinx.serialization.SerializationException
import retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -17,21 +16,12 @@ class VersionDataSourceImpl @Inject constructor(
override suspend fun getVersionInfo(baseUrl: String): VersionInfo { override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
Timber.v("getVersionInfo() called with: baseUrl = $baseUrl") Timber.v("getVersionInfo() called with: baseUrl = $baseUrl")
val service = try { val service = serviceFactory.provideService(baseUrl)
serviceFactory.provideService(baseUrl) val response = runCatchingExceptCancel {
} catch (e: Exception) {
Timber.e(e, "getVersionInfo: can't create service")
throw NetworkError.MalformedUrl(e)
}
val response = try {
service.getVersion() service.getVersion()
} catch (e: Exception) { }.getOrElse {
Timber.e(e, "getVersionInfo: can't request version") Timber.e(it, "getVersionInfo: can't request version")
when (e) { throw it.mapToNetworkError()
is HttpException, is SerializationException -> throw NetworkError.NotMealie(e)
else -> throw NetworkError.NoServerConnection(e)
}
} }
return response.versionInfo() return response.versionInfo()

View File

@@ -1,6 +1,7 @@
package gq.kirmanak.mealient.data.network package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import timber.log.Timber import timber.log.Timber
inline fun <reified T> RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) = inline fun <reified T> RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) =
@@ -14,10 +15,13 @@ class RetrofitServiceFactory<T>(
private val cache: MutableMap<String, T> = mutableMapOf() private val cache: MutableMap<String, T> = mutableMapOf()
override suspend fun provideService(baseUrl: String?): T { override suspend fun provideService(baseUrl: String?): T = runCatchingExceptCancel {
Timber.v("provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}") Timber.v("provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}")
val url = baseUrl ?: baseURLStorage.requireBaseURL() val url = baseUrl ?: baseURLStorage.requireBaseURL()
return synchronized(cache) { cache[url] ?: createService(url, serviceClass) } synchronized(cache) { cache[url] ?: createService(url, serviceClass) }
}.getOrElse {
Timber.e(it, "provideService: can't provide service for $baseUrl")
throw NetworkError.MalformedUrl(it)
} }
private fun createService(url: String, serviceClass: Class<T>): T { private fun createService(url: String, serviceClass: Class<T>): T {

View File

@@ -8,7 +8,7 @@ import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import kotlinx.coroutines.CancellationException import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -39,12 +39,10 @@ class RecipeRepoImpl @Inject constructor(
override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo { override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo {
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug") Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
try { runCatchingExceptCancel {
storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug)) storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug))
} catch (e: CancellationException) { }.onFailure {
throw e Timber.e(it, "loadRecipeInfo: can't update full recipe info")
} catch (e: Throwable) {
Timber.e(e, "loadRecipeInfo: can't update full recipe info")
} }
return storage.queryRecipeInfo(recipeId) return storage.queryRecipeInfo(recipeId)

View File

@@ -7,7 +7,7 @@ import androidx.paging.LoadType.REFRESH
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import kotlinx.coroutines.CancellationException import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -37,16 +37,14 @@ class RecipesRemoteMediator @Inject constructor(
val start = if (loadType == REFRESH) 0 else lastRequestEnd val start = if (loadType == REFRESH) 0 else lastRequestEnd
val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize
val count: Int = try { val count: Int = runCatchingExceptCancel {
val recipes = network.requestRecipes(start, limit) val recipes = network.requestRecipes(start, limit)
if (loadType == REFRESH) storage.refreshAll(recipes) if (loadType == REFRESH) storage.refreshAll(recipes)
else storage.saveRecipes(recipes) else storage.saveRecipes(recipes)
recipes.size recipes.size
} catch (e: CancellationException) { }.getOrElse {
throw e Timber.e(it, "load: can't load recipes")
} catch (e: Throwable) { return MediatorResult.Error(it)
Timber.e(e, "Can't load recipes")
return MediatorResult.Error(e)
} }
// After something is inserted into DB the paging sources have to be invalidated // After something is inserted into DB the paging sources have to be invalidated

View File

@@ -0,0 +1,15 @@
package gq.kirmanak.mealient.extensions
import kotlinx.coroutines.CancellationException
/**
* Like [runCatching] but rethrows [CancellationException] to support
* cancellation of coroutines.
*/
inline fun <T> runCatchingExceptCancel(block: () -> T): Result<T> = try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}

View File

@@ -1,8 +1,11 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.extensions
import gq.kirmanak.mealient.data.network.NetworkError
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
@@ -16,3 +19,7 @@ inline fun <reified T> Json.decodeFromStreamOrNull(stream: InputStream): T? =
.onFailure { Timber.e(it, "decodeFromStreamOrNull: can't decode") } .onFailure { Timber.e(it, "decodeFromStreamOrNull: can't decode") }
.getOrNull() .getOrNull()
fun Throwable.mapToNetworkError(): NetworkError = when (this) {
is HttpException, is SerializationException -> NetworkError.NotMealie(this)
else -> NetworkError.NoServerConnection(this)
}

View File

@@ -6,6 +6,7 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -40,7 +41,7 @@ class AuthenticationViewModel @Inject constructor(
} }
} }
suspend fun authenticate(username: String, password: String): Result<Unit> = runCatching { suspend fun authenticate(username: String, password: String) = runCatchingExceptCancel {
authRepo.authenticate(username, password) authRepo.authenticate(username, password)
}.onFailure { }.onFailure {
Timber.e(it, "authenticate: can't authenticate") Timber.e(it, "authenticate: can't authenticate")

View File

@@ -9,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -41,7 +42,7 @@ constructor(
recipeIngredientsAdapter.submitList(null) recipeIngredientsAdapter.submitList(null)
recipeInstructionsAdapter.submitList(null) recipeInstructionsAdapter.submitList(null)
viewModelScope.launch { viewModelScope.launch {
runCatching { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) } runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) }
.onSuccess { .onSuccess {
Timber.d("loadRecipeInfo: received recipe info = $it") Timber.d("loadRecipeInfo: received recipe info = $it")
_recipeInfo.value = it _recipeInfo.value = it

View File

@@ -71,7 +71,7 @@ class AuthDataSourceImplTest {
@Test(expected = MalformedUrl::class) @Test(expected = MalformedUrl::class)
fun `when authenticate and provideService throws then MalformedUrl`() = runTest { fun `when authenticate and provideService throws then MalformedUrl`() = runTest {
coEvery { authServiceFactory.provideService() } throws RuntimeException() coEvery { authServiceFactory.provideService() } throws MalformedUrl(RuntimeException())
callAuthenticate() callAuthenticate()
} }

View File

@@ -36,7 +36,9 @@ class VersionDataSourceImplTest {
@Test(expected = NetworkError.MalformedUrl::class) @Test(expected = NetworkError.MalformedUrl::class)
fun `when getVersionInfo and provideService throws then MalformedUrl`() = runTest { fun `when getVersionInfo and provideService throws then MalformedUrl`() = runTest {
coEvery { versionServiceFactory.provideService(eq(TEST_BASE_URL)) } throws RuntimeException() coEvery {
versionServiceFactory.provideService(eq(TEST_BASE_URL))
} throws NetworkError.MalformedUrl(RuntimeException())
subject.getVersionInfo(TEST_BASE_URL) subject.getVersionInfo(TEST_BASE_URL)
} }