Simplify error handling
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user