Last active
September 30, 2024 19:26
-
-
Save ElianFabian/ae6833dc6aec026db6ab1de9b2d2dff1 to your computer and use it in GitHub Desktop.
Network utilities for Android.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlinx.coroutines.* | |
import kotlinx.coroutines.flow.* | |
import kotlinx.coroutines.sync.* | |
interface AsyncResource<out T> { | |
sealed interface State<out T> { | |
data object NotStarted : State<Nothing> | |
data class Loading<out T>(val currentResource: Completed<T>?) : State<T> | |
sealed interface Completed<out T> : State<T> { | |
data class Success<T>(val data: T) : Completed<T> | |
data class Error(val throwable: Throwable) : Completed<Nothing> | |
} | |
data class Cancelled<out T>(val currentResource: Completed<T>?) : State<T> | |
data class Empty<out T>( | |
val previousResource: Completed<T>?, | |
val wasCancelled: Boolean, | |
) : State<T> | |
} | |
val state: StateFlow<State<T>> | |
fun startFetching() | |
fun cancel() | |
fun clear() | |
} | |
suspend fun <T> AsyncResource<T>.awaitLoading() { | |
state.first { it is AsyncResource.State.Loading<T> } | |
} | |
suspend fun <T> AsyncResource<T>.awaitCompletion() { | |
state.first { | |
it is AsyncResource.State.Completed<T> | |
|| it is AsyncResource.State.Cancelled<T> | |
|| it is AsyncResource.State.Empty<T> | |
} | |
} | |
val <T> AsyncResource<T>.hasNotStarted: Boolean get() = state.value is AsyncResource.State.NotStarted | |
val <T> AsyncResource<T>.isLoading: Boolean get() = state.value is AsyncResource.State.Loading<T> | |
val <T> AsyncResource<T>.isSuccess: Boolean get() = state.value is AsyncResource.State.Completed.Success<T> | |
val AsyncResource<*>.isError: Boolean get() = state.value is AsyncResource.State.Completed.Error | |
val <T> AsyncResource<T>.isCancelled: Boolean get() = state.value is AsyncResource.State.Cancelled<T> | |
val <T> AsyncResource<T>.isCompleted: Boolean get() = state.value is AsyncResource.State.Completed<T> | |
val <T> AsyncResource<T>.isEmpty: Boolean get() = state.value is AsyncResource.State.Empty<T> | |
fun <T> StateFlow<AsyncResource.State<T>>.asResourceFlow(): Flow<AsyncResource.State.Completed<T>?> { | |
return map { state -> | |
when (state) { | |
is AsyncResource.State.Loading<T> -> state.currentResource | |
is AsyncResource.State.Completed<T> -> state | |
is AsyncResource.State.Cancelled<T> -> state.currentResource | |
else -> null | |
} | |
} | |
} | |
fun <T> StateFlow<AsyncResource.State<T>>.asLoadingFlow(): Flow<AsyncResource.State.Loading<T>?> { | |
return map { state -> | |
if (state is AsyncResource.State.Loading<T>) state else null | |
} | |
} | |
private class AsyncResourceImpl<T : Any>( | |
private val scope: CoroutineScope, | |
private val getData: suspend () -> T, | |
) : AsyncResource<T> { | |
private val _state = MutableStateFlow<AsyncResource.State<T>>(AsyncResource.State.NotStarted) | |
override val state = _state.asStateFlow() | |
private var _currentResource: AsyncResource.State.Completed<T>? = null | |
private var _job: Job? = null | |
private val _jobMutex = Mutex() | |
override fun startFetching() { | |
scope.launch { | |
_jobMutex.withLock { | |
_state.value = AsyncResource.State.Loading(_currentResource) | |
_job?.cancelAndJoin() | |
_job = launch { | |
_state.value = try { | |
val data = getData() | |
val resource = AsyncResource.State.Completed.Success(data) | |
_currentResource = resource | |
resource | |
} | |
catch (e: CancellationException) { | |
throw e | |
} | |
catch (e: Throwable) { | |
val resource = AsyncResource.State.Completed.Error(e) | |
_currentResource = resource | |
resource | |
} | |
finally { | |
_job = null | |
} | |
} | |
} | |
} | |
} | |
override fun cancel() { | |
scope.launch { | |
_jobMutex.withLock { | |
val job = _job ?: return@withLock | |
job.cancel() | |
_job = null | |
_state.value = AsyncResource.State.Cancelled(_currentResource) | |
} | |
} | |
} | |
override fun clear() { | |
scope.launch { | |
_jobMutex.withLock { | |
val job = _job | |
job?.cancel() | |
val wasCancelled = job?.isCancelled == true | |
_job = null | |
if (_state.value is AsyncResource.State.Empty) { | |
return@withLock | |
} | |
_state.value = AsyncResource.State.Empty( | |
previousResource = _currentResource, | |
wasCancelled = wasCancelled, | |
) | |
_currentResource = null | |
} | |
} | |
} | |
} | |
fun <T : Any> asyncResource( | |
scope: CoroutineScope, | |
getData: suspend () -> T, | |
): AsyncResource<T> { | |
return AsyncResourceImpl(scope, getData) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sealed interface NetworkRequestError { | |
data class HttpError( | |
val code: Int, | |
val message: String?, | |
) : NetworkRequestError | |
data object IoError : NetworkRequestError | |
data object UnknownError : NetworkRequestError | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlin.contracts.ExperimentalContracts | |
import kotlin.contracts.contract | |
sealed interface NetworkResult<out T : Any> { | |
sealed interface Failure : NetworkResult<Nothing> | |
data object NoInternetConnection : Failure | |
data class RequestError(val error: NetworkRequestError) : Failure | |
data class Success<out T : Any>(val data: T) : NetworkResult<T> | |
} | |
@OptIn(ExperimentalContracts::class) | |
fun <T : Any> NetworkResult<T>.isSuccessful(): Boolean { | |
contract { | |
returns(true) implies (this@isSuccessful is NetworkResult.Success<T>) | |
returns(false) implies (this@isSuccessful is NetworkResult.Failure) | |
} | |
return this is NetworkResult.Success<T> | |
} | |
@OptIn(ExperimentalContracts::class) | |
fun <T : Any> NetworkResult<T>.isFailure(): Boolean { | |
contract { | |
returns(false) implies (this@isFailure is NetworkResult.Success<T>) | |
returns(true) implies (this@isFailure is NetworkResult.Failure) | |
} | |
return this is NetworkResult.Failure | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sealed interface Resource<out D, out E> { | |
class Success<out D>(val data: D) : Resource<D, Nothing> | |
class Error<out E>(val error: E) : Resource<Nothing, E> | |
companion object | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import retrofit2.HttpException | |
import java.io.IOException | |
import kotlin.coroutines.cancellation.CancellationException | |
suspend inline fun <D> Resource.Companion.safeApiCall( | |
crossinline call: suspend () -> D, | |
): Resource<D, NetworkRequestError> { | |
try { | |
val response = call() | |
return Resource.Success(response) | |
} | |
catch (e: CancellationException) { | |
throw e | |
} | |
catch (e: HttpException) { | |
e.printStackTrace() | |
return Resource.Error( | |
NetworkRequestError.HttpError( | |
code = e.code(), | |
message = e.message().takeIf { it.isNotBlank() }, | |
) | |
) | |
} | |
catch (e: IOException) { | |
e.printStackTrace() | |
return Resource.Error(NetworkRequestError.IoError) | |
} | |
catch (e: Exception) { | |
e.printStackTrace() | |
return Resource.Error(NetworkRequestError.UnknownError) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment