Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active September 30, 2024 19:26
Show Gist options
  • Save ElianFabian/ae6833dc6aec026db6ab1de9b2d2dff1 to your computer and use it in GitHub Desktop.
Save ElianFabian/ae6833dc6aec026db6ab1de9b2d2dff1 to your computer and use it in GitHub Desktop.
Network utilities for Android.
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)
}
sealed interface NetworkRequestError {
data class HttpError(
val code: Int,
val message: String?,
) : NetworkRequestError
data object IoError : NetworkRequestError
data object UnknownError : NetworkRequestError
}
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
}
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
}
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