Created
April 12, 2022 12:11
-
-
Save ildar2/7db73ed0b721d4cc441f3e5e2d8a526b to your computer and use it in GitHub Desktop.
This file contains 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 androidx.lifecycle.ViewModel | |
import kotlinx.coroutines.* | |
import kotlinx.coroutines.flow.* | |
import org.koin.core.component.KoinComponent | |
import org.koin.core.component.inject | |
import org.koin.core.qualifier.named | |
import kotlin.coroutines.CoroutineContext | |
/** | |
* Base ViewModel class for MVVM | |
* | |
* uses kotlin coroutines | |
* | |
* has: | |
* - [contextProvider] - provides IO or Main context for coroutines | |
* - [coroutineJob] - parent Job for all running coroutines, see [stop] | |
* - [scope] - coroutine scope | |
* - [uiCaller] - helper class to make IO-operations (network, data bases) | |
* | |
* MVVM Views (fragments, compose screens) should observe: | |
* - [UiCaller.errorSharedFlow] to show errors | |
* - [UiCaller.statusStateFlow] to track loading status (can be customized) | |
*/ | |
abstract class BaseViewModel( | |
protected val contextProvider: ContextProvider = ContextProvider(), | |
private val coroutineJob: Job = SupervisorJob(), | |
protected val scope: CoroutineScope = CoroutineScope(coroutineJob + contextProvider.IO), | |
protected val uiCaller: UiCallerImpl = UiCallerImpl(scope, contextProvider) | |
) : ViewModel(), UiCaller by uiCaller { | |
open fun stop() { | |
coroutineJob.cancelChildren() | |
} | |
override fun onCleared() { | |
super.onCleared() | |
coroutineJob.cancel() | |
} | |
protected infix fun <T> FlowCollector<T>.emit(value: T) { | |
scope.launch { emit(value) } | |
} | |
} | |
/** | |
* Interface to communicate with MVVM views | |
*/ | |
interface UiCaller { | |
val statusStateFlow: StateFlow<Status> | |
val errorSharedFlow: SharedFlow<String> | |
} | |
/** | |
* Helper class, incapsulating request handling logic | |
* can be distributed to ViewModel extentions (composition) | |
* | |
* - makes IO-requests: | |
* - [makeRequest] with helper fun [unwrap] | |
* - [makeRequestUnwrapped] | |
* - [makeRequestFor] | |
* - sends errors to [errorSharedFlow] | |
* @see setError | |
* - shows loading state via [statusStateFlow] | |
* even for multiple requests | |
* @see set | |
*/ | |
class UiCallerImpl( | |
val scope: CoroutineScope, | |
private val contextProvider: ContextProvider = ContextProvider(), | |
) : UiCaller { | |
private val _statusStateFlow: MutableStateFlow<Status> = MutableStateFlow(Status.HIDE_LOADING) | |
override val statusStateFlow: StateFlow<Status> = _statusStateFlow | |
private val _errorSharedFlow: MutableSharedFlow<String> = MutableSharedFlow() | |
override val errorSharedFlow: SharedFlow<String> = _errorSharedFlow | |
/** | |
* Presentation layer handler for requests | |
* launches [Job] in [scope], | |
* sets loading state on [_statusStateFlow] | |
* | |
* [call] - suspend repository function | |
* [statusFlow] - can be customized with different stateFlows (or null) | |
* [resultBlock] - function to handle result (called in Main) | |
* | |
* returns [Job] for potential canceling | |
*/ | |
fun <T> makeRequest( | |
call: suspend CoroutineScope.() -> T, | |
statusFlow: MutableStateFlow<Status>? = _statusStateFlow, | |
resultBlock: (suspend (T) -> Unit)? | |
): Job = scope.launch(contextProvider.Main) { | |
set(Status.SHOW_LOADING, statusFlow) | |
try { | |
val result = withContext(contextProvider.IO, call) | |
resultBlock?.invoke(result) | |
} catch (e: Throwable) { | |
if (e !is CancellationException) { | |
setError(e.message.orEmpty()) | |
} | |
} | |
set(Status.HIDE_LOADING, statusFlow) | |
} | |
/** | |
* To keep track of multiple [makeRequest] calls | |
*/ | |
private var requestCounter = 0 | |
/** | |
* Setting loading state for [statusFlow] | |
* [_statusStateFlow] by default | |
* can be customized with different statusFlow or `null` | |
*/ | |
private fun set(status: Status, statusFlow: MutableStateFlow<Status>?) { | |
statusFlow ?: return | |
if (statusFlow === _statusStateFlow) { | |
when (status) { | |
Status.SHOW_LOADING -> { | |
requestCounter++ | |
} | |
Status.HIDE_LOADING -> { | |
requestCounter-- | |
if (requestCounter > 0) return | |
requestCounter = 0 | |
} | |
} | |
} | |
statusFlow.value = status | |
} | |
fun setError(error: String) { | |
scope.launch { | |
_errorSharedFlow.emit(error) | |
} | |
} | |
fun <T> makeRequestUnwrapped( | |
call: suspend CoroutineScope.() -> RequestResult<T>, | |
statusFlow: MutableStateFlow<Status>? = _statusStateFlow, | |
resultBlock: (suspend (T) -> Unit)? | |
): Job = scope.launch(contextProvider.Main) { | |
set(Status.SHOW_LOADING, statusFlow) | |
try { | |
when (val result = withContext(contextProvider.IO, call)) { | |
is RequestResult.Error -> setError(result.error) | |
is RequestResult.Success -> resultBlock?.invoke(result.result) | |
} | |
} catch (e: Throwable) { | |
if (e !is CancellationException) { | |
setError(e.message.orEmpty()) | |
} | |
} | |
set(Status.HIDE_LOADING, statusFlow) | |
} | |
/** | |
* Helper function to unwrap [RequestResult] | |
*/ | |
fun <T> unwrap( | |
result: RequestResult<T>, | |
errorBlock: ((String) -> Unit)?, | |
successBlock: (T) -> Unit | |
) = when (result) { | |
is RequestResult.Success -> result.result?.let { successBlock(it) } | |
is RequestResult.Error -> errorBlock?.invoke(result.error) | |
} | |
fun <T> makeRequestFor( | |
statusStateFlow: MutableStateFlow<T>, | |
statusFlow: MutableStateFlow<Status>? = _statusStateFlow, | |
call: suspend CoroutineScope.() -> RequestResult<T> | |
) = makeRequest(call, statusFlow) { | |
when (it) { | |
is RequestResult.Success<T> -> statusStateFlow.value = it.result | |
is RequestResult.Error -> setError(it.error) | |
} | |
} | |
} | |
/** | |
* Used in [BaseViewModel] to make coroutine scope | |
* should be mocked in tests with [Dispatchers.Unconfined] | |
*/ | |
class ContextProvider : KoinComponent { | |
val Main: CoroutineContext by inject(named("main"))//Dispatchers.Main | |
val IO: CoroutineContext by inject(named("io"))//Dispatchers.IO | |
} | |
/** | |
* Loading status | |
* @see UiCaller | |
*/ | |
enum class Status { | |
SHOW_LOADING, | |
HIDE_LOADING, | |
} | |
/** | |
* Result wrapper for presentation layer | |
* should be returned by repositories using [CoroutineCaller] | |
*/ | |
sealed class RequestResult<out T : Any?> { | |
data class Success<out T : Any?>(val result: T) : RequestResult<T>() | |
data class Error(val error: String, val code: Int = 0) : RequestResult<Nothing>() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment