Creating a Kotlin DSL to accept processing pipeline and deliver an end result - android

I want to try to write a DSL, which can launch multiple methods piped one after the other to give an end result of a specific type, lets call it “Result”. The intermediate results of each of the calls can have different types, lets call them for example “Type 1", “Type 2” etc. I tried to accomplish something like this with following code.
class PipelineBuilder<I, R> {
val steps = mutableListOf<suspend (Any?) -> Any?>()
var initialFunction: (suspend () -> I)? = null
fun startWith(step: suspend () -> I) {
initialFunction = step
}
inline fun <reified T> then(noinline step: suspend (T) -> Any) {
steps.add { step }
}
suspend fun execute(): Any? {
if (steps.isEmpty()) {
throw Exception("Your pipeline is empty.")
}
var result: Any? = initialFunction?.invoke()
if (steps.size == 1) {
return result
}
for (index in 0 until steps.size) {
result = steps[index].invoke(result)
}
return result
}
suspend fun <T> executeConcurrent(vararg steps: suspend () -> T): List<T> {
return steps.map { step ->
coroutineScope {
async { step() }
}
}.awaitAll()
}
}
suspend fun <I, R> pipeline(block: suspend PipelineBuilder<I, R>.() -> Unit): Any? {
val builder = PipelineBuilder<I, R>()
builder.block()
return builder.execute()
}
suspend fun type1Method(input: Int): Type1 {
delay(1000)
return Type1()
}
suspend fun type2Method(input: Type1): Type2 {
delay(1000)
return Type2()
}
suspend fun type3Method(input: Type2): Int {
delay(1000)
return 3
}
class Type1
class Type2
class Type3
fun main() {
runBlocking {
val result = pipeline<Type1, Int> {
startWith { type1Method(1) }
then<Type1> {
type2Method(it)
}
then<Type2> {
type3Method(it)
}
}
println(result)
}
}
The pipeline should be able to execute multiple steps concurrently as well and combine the results in a combined result. When calling execute, you should execute the entire pipeline and then get an end result.
When I execute the code above, I don't get a "3" as a result, but "Function2<com.unisoft.myapplication.Type2, kotlin.coroutines.Continuation<? super java.lang.Object>, java.lang.Object>
". Where did I make a mistake and is there a simpler way of achieving this?

Related

MutableSharedFlow not trigger same event twice in kotlin

I am using MutableSharedFlow in project. My main project concept is very big, so I cannot add in here, instead I made a very small sample to reproduce my problem. I know this example is very wrong, but I have same scenario in my main project. I am using MutableSharedFlow as a Queue implementation with single Thread execution with the help of Mutex.
ExampleViewModel
class ExampleViewModel : ViewModel() {
val serviceNumber = ServiceNumber()
val serviceNumberEventFlow = serviceNumber.eventFlow
val mutex = Mutex()
var delayCounter = 0
suspend fun addItem(itemOne: Int = 2, itemTwo: Int = 2): Add {
return mutex.queueWithTimeout("add") {
serviceNumberEventFlow.onSubscription {
serviceNumber.add(itemOne, itemTwo)
delayCounter++
if (delayCounter == 1) {
delay(1000)
Log.w("Delay ", "Delay Started")
serviceNumber.add(8, 8)
}
}.firstOrNull {
it is Add
} as Add? ?: Add("No value")
}
}
suspend fun subItem(itemOne: Int = 2, itemTwo: Int = 2): Sub {
return mutex.queueWithTimeout("sub") {
serviceNumberEventFlow.onSubscription {
serviceNumber.sub(itemOne, itemTwo)
}.firstOrNull {
it is Sub
} as Sub? ?: Sub("No value")
}
}
private suspend fun <T> Mutex.queueWithTimeout(
action: String, timeout: Long = 5000L, block: suspend CoroutineScope.() -> T
): T {
return try {
withLock {
return#withLock withTimeout<T>(timeMillis = timeout, block = block)
}
} catch (e: Exception) {
Log.e("Wrong", " $e Timeout on BLE call: $action")
throw e
}
}
}
class ServiceNumber : Number {
val eventFlow = MutableSharedFlow<Event>(extraBufferCapacity = 50)
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun add(itemOne: Int, itemTwo: Int) {
Log.i("ServiceNumber", " Add event trigger with $itemOne -- $itemTwo")
eventFlow.emitEvent(Add("Item added ${itemOne + itemTwo}"))
}
override fun sub(itemOne: Int, itemTwo: Int) {
eventFlow.emitEvent(Sub("Item subtract ${itemOne - itemTwo}"))
}
private fun <T> MutableSharedFlow<T>.emitEvent(event: T) {
scope.launch { emit(event) }
}
}
interface Number {
fun add(itemOne: Int, itemTwo: Int)
fun sub(itemOne: Int, itemTwo: Int)
}
sealed class Event
data class Add(val item: String) : Event()
data class Sub(val item: String) : Event()
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val viewModel: ExampleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Theme {
Column {
Button(onClick = {
lifecycleScope.launchWhenCreated {
withContext(Dispatchers.IO) {
val result = viewModel.addItem()
Log.e("Result", "$result")
}
}
}) {
Text("Add")
}
Button(onClick = {
lifecycleScope.launchWhenCreated {
withContext(Dispatchers.IO) {
val result = viewModel.subItem()
Log.e("Result", "$result")
}
}
}) {
Text("Sub")
}
}
}
}
}
}
#Composable
fun Theme(content: #Composable () -> Unit) {
MaterialTheme(content = content)
}
Problem
This example is simple Add and subtract of two number. When I am click on Add Button first time, viewmodel.addItem(...) -> ... ->ServiceNumber.add() will trigger and emit the value and we can see log in console. Inside the Add Button function, I was also added a delay to trigger ServiceNumber.add() again to see that onSubscription will be also retrigger or not. MutableSharedFlow emit the value as I can see in log but onSubscription method not called. I don't understand what is the problem in here.
onSubscription is an operator so it creates a new copy of your shared flow. The lambda code will only be run when there are new collectors on this new flow. The only time you collect this new flow is when you call firstOrNull() on it, a terminal operator that collects a single value.

Run two suspend functions in parallel and return when first one returns

I want to run to suspend functions in parallel and return result once the faster one of them returns result.
I tried doing the following, but I think it only returns when both of them have finished and it takes too much time, since I'm waiting for both.
val coroutineScope = CoroutineScope(Dispatchers.Main)
val a = coroutineScope.async {
a(context)
}
val b = coroutineScope.async {
b(context)
}
val results = listOf(a, b).awaitAll()
return if (results.any { it is RunSuccess }) {
...
} else {
...
}
Any ideas?
You can use select as follows :
suspend fun doWork(): String = coroutineScope {
select<String> {
async { work1() }.onAwait { it }
async { work2() }.onAwait { it }
}.also {
coroutineContext.cancelChildren()
}
}
On this example is returning a String but you can change it with whatever you want, it depends on what your work is returning.
In case you are looking for more functional programming version you can use raceN from Arrow
Where you have this method
public suspend inline fun <A, B> raceN(crossinline fa: suspend CoroutineScope.() -> A, crossinline fb: suspend CoroutineScope.() -> B): Either<A, B> =
raceN(Dispatchers.Default, fa, fb)
And then you call this raceN method
public suspend inline fun <A, B> raceN(
ctx: CoroutineContext = EmptyCoroutineContext,
crossinline fa: suspend CoroutineScope.() -> A,
crossinline fb: suspend CoroutineScope.() -> B
): Either<A, B> =
coroutineScope {
val a = async(ctx) { fa() }
val b = async(ctx) { fb() }
select<Either<A, B>> {
a.onAwait.invoke { Either.Left(it) }
b.onAwait.invoke { Either.Right(it) }
}.also {
when (it) {
is Either.Left -> b.cancelAndJoin()
is Either.Right -> a.cancelAndJoin()
}
}
}
Arrow fx has a nice method called raceN.
https://arrow-kt.io/docs/apidocs/arrow-fx-coroutines/arrow.fx.coroutines/race-n.html

UseCases or Interactors with Kt Flow and Retrofit

Context
I started working on a new project and I've decided to move from RxJava to Kotlin Coroutines. I'm using an MVVM clean architecture, meaning that my ViewModels communicate to UseCases classes, and these UseCases classes use one or many Repositories to fetch data from network.
Let me give you an example. Let's say we have a screen that is supposed to show the user profile information. So we have the UserProfileViewModel:
#HiltViewModel
class UserProfileViewModel #Inject constructor(
private val getUserProfileUseCase: GetUserProfileUseCase
) : ViewModel() {
sealed class State {
data SuccessfullyFetchedUser(
user: ExampleUser
) : State()
}
// ...
val state = SingleLiveEvent<UserProfileViewModel.State>()
// ...
fun fetchUserProfile() {
viewModelScope.launch {
// ⚠️ We trigger the use case to fetch the user profile info
getUserProfileUseCase()
.collect {
when (it) {
is GetUserProfileUseCase.Result.UserProfileFetched -> {
state.postValue(State.SuccessfullyFetchedUser(it.user))
}
is GetUserProfileUseCase.Result.ErrorFetchingUserProfile -> {
// ...
}
}
}
}
}
}
The GetUserProfileUseCase use case would look like this:
interface GetUserProfileUseCase {
sealed class Result {
object ErrorFetchingUserProfile : Result()
data class UserProfileFetched(
val user: ExampleUser
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class GetUserProfileUseCaseImpl(
private val userRepository: UserRepository
) : GetUserProfileUseCase {
override suspend fun invoke(email: String): Flow<GetUserProfileUseCase.Result> {
// ⚠️ Hit the repository to fetch the info. Notice that if we have more
// complex scenarios, we might require zipping repository calls together, or
// flatmap responses.
return userRepository.getUserProfile().flatMapMerge {
when (it) {
is ResultData.Success -> {
flow { emit(GetUserProfileUseCase.Result.UserProfileFetched(it.data.toUserExampleModel())) }
}
is ResultData.Error -> {
flow { emit(GetUserProfileUseCase.Result.ErrorFetchingUserProfile) }
}
}
}
}
}
The UserRepository repository would look like this:
interface UserRepository {
fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>>
}
class UserRepositoryImpl(
private val retrofitApi: RetrofitApi
) : UserRepository {
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>> {
return flow {
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful) {
emit(ResultData.Success(response.body()!!))
} else {
emit(ResultData.Error(RetrofitNetworkError(response.code())))
}
}
}
}
And finally, the RetrofitApi and the response class to model the backend API response would look like this:
data class ApiUserProfileResponse(
#SerializedName("user_name") val userName: String
// ...
)
interface RetrofitApi {
#GET("api/user/profile")
suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
}
Everything has been working fine so far, but I've started to run into some issues when implementing more complex features.
For example, there's a use case where I need to (1) post to a POST /send_email_link endpoint when the user first signs in, this endpoint will check if the email that I send in the body already exists, if it doesn't it will return a 404 error code, and (2) if everything goes okay, I'm supposed to hit a POST /peek endpoint that will return some info about the user account.
This is what I've implemented so far for this UserAccountVerificationUseCase:
interface UserAccountVerificationUseCase {
sealed class Result {
object ErrorVerifyingUserEmail : Result()
object ErrorEmailDoesNotExist : Result()
data class UserEmailVerifiedSuccessfully(
val canSignIn: Boolean
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return userRepository.postSendEmailLink().flatMapMerge {
when (it) {
is ResultData.Success -> {
userRepository.postPeek().flatMapMerge {
when (it) {
is ResultData.Success -> {
val canSignIn = it.data?.userName == "Something"
flow { emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn)) }
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
}
}
is ResultData.Error -> {
if (it.exception is RetrofitNetworkError) {
if (it.exception.errorCode == 404) {
flow { emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist) }
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
}
}
}
}
Issue
The above solution is working as expected, if the first API call to the POST /send_email_link ever returns a 404, the use case will behave as expected and return the ErrorEmailDoesNotExist response so the ViewModel can pass that back to the UI and show the expected UX.
The problem as you can see is that this solution requires a ton of boilerplate code, I thought using Kotlin Coroutines would make things simpler than with RxJava, but it hasn't turned out like that yet. I'm quite sure that this is because I'm missing something or I haven't quite learned how to use Flow properly.
What I've tried so far
I've tried to change the way I emit the elements from the repositories, from this:
...
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>> {
return flow {
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful) {
emit(ResultData.Success(response.body()!!))
} else {
emit(ResultData.Error(RetrofitNetworkError(response.code())))
}
}
}
...
To something like this:
...
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>> {
return flow {
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful) {
emit(ResultData.Success(response.body()!!))
} else {
error(RetrofitNetworkError(response.code()))
}
}
}
..
So I can use the catch() function like I'd with RxJava's onErrorResume():
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return userRepository.postSendEmailLink()
.catch { e ->
if (e is RetrofitNetworkError) {
if (e.errorCode == 404) {
flow { emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist) }
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
.flatMapMerge {
userRepository.postPeek().flatMapMerge {
when (it) {
is ResultData.Success -> {
val canSignIn = it.data?.userName == "Something"
flow { emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn)) }
} else -> {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
}
}
}
}
}
This does reduce the boilerplate code a bit, but I haven't been able to get it working because as soon as I try to run the use case like this I start getting errors saying that I shouldn't emit items in the catch().
Even if I could get this working, still, there's way too much boilerplate code here. I though doing things like this with Kotlin Coroutines would mean having much more simple, and readable, use cases. Something like:
...
class UserAccountVerificationUseCaseImpl(
private val userRepository: AuthRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return flow {
coroutineScope {
val sendLinksResponse = userRepository.postSendEmailLink()
if (sendLinksResponse is ResultData.Success) {
val peekAccount = userRepository.postPeek()
if (peekAccount is ResultData.Success) {
emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully())
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
} else {
if (sendLinksResponse is ResultData.Error) {
if (sendLinksResponse.error == 404) {
emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
}
}
}
}
}
...
This is what I had pictured about working with Kotlin Coroutines. Ditching RxJava's zip(), contact(), delayError(), onErrorResume() and all those Observable functions in favor of something more readable.
Question
How can I reduce the amount of boilerplate code and make my use cases look more Coroutine-like?
Notes
I know some people just call the repositories directly from the ViewModel layer, but I like having this UseCase layer in the middle so I can contain all the code related to switching streams and handling errors here.
Any feedback is appreciated! Thanks!
Edit #1
Based on #Joffrey response, I've changed the code so it works like this:
The Retrofit API layer keeps returning suspendable function.
data class ApiUserProfileResponse(
#SerializedName("user_name") val userName: String
// ...
)
interface RetrofitApi {
#GET("api/user/profile")
suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
}
The repository now returns a suspendable function and I've removed the Flow wrapper:
interface UserRepository {
suspend fun getUserProfile(): ResultData<ApiUserProfileResponse>
}
class UserRepositoryImpl(
private val retrofitApi: RetrofitApi
) : UserRepository {
override suspend fun getUserProfile(): ResultData<ApiUserProfileResponse> {
val response = retrofitApi.getUserProfileFromApi()
return if (response.isSuccessful) {
ResultData.Success(response.body()!!)
} else {
ResultData.Error(RetrofitNetworkError(response.code()))
}
}
}
The use case keeps returning a Flow since I might also plug calls to a Room DB here:
interface GetUserProfileUseCase {
sealed class Result {
object ErrorFetchingUserProfile : Result()
data class UserProfileFetched(
val user: ExampleUser
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class GetUserProfileUseCaseImpl(
private val userRepository: UserRepository
) : GetUserProfileUseCase {
override suspend fun invoke(email: String): Flow<GetUserProfileUseCase.Result> {
return flow {
val userProfileResponse = userRepository.getUserProfile()
when (userProfileResponse) {
is ResultData.Success -> {
emit(GetUserProfileUseCase.Result.UserProfileFetched(it.toUserModel()))
}
is ResultData.Error -> {
emit(GetUserProfileUseCase.Result.ErrorFetchingUserProfile)
}
}
}
}
}
This looks much more clean. Now, applying the same thing to the UserAccountVerificationUseCase:
interface UserAccountVerificationUseCase {
sealed class Result {
object ErrorVerifyingUserEmail : Result()
object ErrorEmailDoesNotExist : Result()
data class UserEmailVerifiedSuccessfully(
val canSignIn: Boolean
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return flow {
val sendEmailLinkResponse = userRepository.postSendEmailLink()
when (sendEmailLinkResponse) {
is ResultData.Success -> {
val peekResponse = userRepository.postPeek()
when (peekResponse) {
is ResultData.Success -> {
val canSignIn = peekResponse.data?.userName == "Something"
emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn)
}
else -> {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
}
}
is ResultData.Error -> {
if (sendEmailLinkResponse.isNetworkError(404)) {
emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
}
}
}
}
}
This looks much more clean and it works perfectly. I still wonder if there's any more room for improvement here.
The most obvious problem I see here is that you're using Flow for single values instead of suspend functions.
Coroutines makes the single-value use case much simpler by using suspend functions that return plain values or throw exceptions. You can of course also make them return Result-like classes to encapsulate errors instead of actually using exceptions, but the important part is that with suspend functions you are exposing a seemingly synchronous (thus convenient) API while still benefitting from asynchronous runtime.
In the provided examples you're not subscribing for updates anywhere, all flows actually just give a single element and complete, so there is no real reason to use flows and it complicates the code. It also makes it harder to read for people used to coroutines because it looks like multiple values are coming, and potentially collect being infinite, but it's not the case.
Each time you write flow { emit(x) } it should just be x.
Following the above, you're sometimes using flatMapMerge and in the lambda you create flows with a single element. Unless you're looking for parallelization of the computation, you should simply go for .map { ... } instead. So replace this:
val resultingFlow = sourceFlow.flatMapMerge {
if (something) {
flow { emit(x) }
} else {
flow { emit(y) }
}
}
With this:
val resultingFlow = sourceFlow.map { if (something) x else y }

android rxjava2 zip vararg parameter

I m now working on about the rxjava2 in android kotlin . And try to write a function to zip multiple observable and observer. But it seems like some mistake. Can anyone help?
First , i try to write zip 2 observable and it works. but when I want to extend it to vararg , it fails.
fun <T> ApiSubscribeZip2(observable1: Observable<T>, observable2: Observable<T>, observer: Observer<List<T>>) {
Observable.zip(observable1, observable2, BiFunction<T, T, List<T>> { t1: T, t2: T ->
zipAdd(t1, t2)
})
?.subscribeOn(Schedulers.io())?.unsubscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(observer as Observer<in List<T>>)
}
fun <T> ApiSubscribeZipN(vararg observable: Observable<T>?, observer: Observer<List<T>>) {
Observable.zip(observable, Function<T, List<T>> { it ->
zipAdd(it)
})
}
private fun <T> zipAdd(vararg observableType: T): List<T> {
val list = ArrayList<T>()
for (ob in observableType) {
list.add(ob)
}
return list
}
the apisubscribezipN shows that None of the following function can be called with the arguments supplied.
You can use Observable.zipIterable like this:
fun <T> ApiSubscribeZipN(vararg observable: Observable<T>?, observer: Observer<List<T>>) {
Observable.zipIterable<T, List<T>>(observable.filterNotNull().toList(), { it.toList() as List<T>? }, false, 100)
.subscribeOn(Schedulers.io()).unsubscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.io())
.subscribe(observer)
}
What about this?
import io.reactivex.Observable
import io.reactivex.functions.Function
import org.junit.Test
fun <T> apiSubscribeZipN(vararg observable: Observable<T>?): Observable<List<T>> {
val filterNotNull = observable.filterNotNull()
return Observable.zip(filterNotNull, Function { inArr ->
inArr.map {
it as T
}
})
}
Test
#Test
fun whatever() {
val mergeWith1 = Observable.fromArray("test11", "test12").mergeWith(Observable.never())
val mergeWith2 = Observable.fromArray("test21", "test22").mergeWith(Observable.never())
apiSubscribeZipN(mergeWith1, mergeWith2)
.test()
.assertValues(listOf("test11", "test21"), listOf("test12", "test22"))
}

NetworkBoundResource with Kotlin coroutines

Do you have any ideas how to implement repository pattern with NetworkBoundResource and Kotlin coroutines? I know we can launch a coroutine withing a GlobalScope, but it may lead to coroutine leak. I would like to pass a viewModelScope as a parameter, but it is a bit tricky, when it comes to implementation (because my repository doesn't know a CoroutineScope of any ViewModel).
abstract class NetworkBoundResource<ResultType, RequestType>
#MainThread constructor(
private val coroutineScope: CoroutineScope
) {
private val result = MediatorLiveData<Resource<ResultType>>()
init {
result.value = Resource.loading(null)
#Suppress("LeakingThis")
val dbSource = loadFromDb()
result.addSource(dbSource) { data ->
result.removeSource(dbSource)
if (shouldFetch(data)) {
fetchFromNetwork(dbSource)
} else {
result.addSource(dbSource) { newData ->
setValue(Resource.success(newData))
}
}
}
}
#MainThread
private fun setValue(newValue: Resource<ResultType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork(dbSource: LiveData<ResultType>) {
val apiResponse = createCall()
result.addSource(dbSource) { newData ->
setValue(Resource.loading(newData))
}
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
result.removeSource(dbSource)
when (response) {
is ApiSuccessResponse -> {
coroutineScope.launch(Dispatchers.IO) {
saveCallResult(processResponse(response))
withContext(Dispatchers.Main) {
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
}
is ApiEmptyResponse -> {
coroutineScope.launch(Dispatchers.Main) {
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
is ApiErrorResponse -> {
onFetchFailed()
result.addSource(dbSource) { newData ->
setValue(Resource.error(response.errorMessage, newData))
}
}
}
}
}
}
Update (2020-05-27):
A way which is more idiomatic to the Kotlin language than my previous examples, uses the Flow APIs, and borrows from Juan's answer can be represented as a standalone function like the following:
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { Unit },
crossinline shouldFetch: (ResultType) -> Boolean = { true }
) = flow<Resource<ResultType>> {
emit(Resource.Loading(null))
val data = query().first()
val flow = if (shouldFetch(data)) {
emit(Resource.Loading(data))
try {
saveFetchResult(fetch())
query().map { Resource.Success(it) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
query().map { Resource.Error(throwable, it) }
}
} else {
query().map { Resource.Success(it) }
}
emitAll(flow)
}
The above code can be called from a class, e.g. a Repository, like so:
fun getItems(request: MyRequest): Flow<Resource<List<MyItem>>> {
return networkBoundResource(
query = { dao.queryAll() },
fetch = { retrofitService.getItems(request) },
saveFetchResult = { items -> dao.insert(items) }
)
}
Original answer:
This is how I've been doing it using the livedata-ktx artifact; no need to pass in any CoroutineScope. The class also uses just one type instead of two (e.g. ResultType/RequestType) since I always end up using an adapter elsewhere for mapping those.
import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import nihk.core.Resource
// Adapted from: https://developer.android.com/topic/libraries/architecture/coroutines
abstract class NetworkBoundResource<T> {
fun asLiveData() = liveData<Resource<T>> {
emit(Resource.Loading(null))
if (shouldFetch(query())) {
val disposable = emitSource(queryObservable().map { Resource.Loading(it) })
try {
val fetchedData = fetch()
// Stop the previous emission to avoid dispatching the saveCallResult as `Resource.Loading`.
disposable.dispose()
saveFetchResult(fetchedData)
// Re-establish the emission as `Resource.Success`.
emitSource(queryObservable().map { Resource.Success(it) })
} catch (e: Exception) {
onFetchFailed(e)
emitSource(queryObservable().map { Resource.Error(e, it) })
}
} else {
emitSource(queryObservable().map { Resource.Success(it) })
}
}
abstract suspend fun query(): T
abstract fun queryObservable(): LiveData<T>
abstract suspend fun fetch(): T
abstract suspend fun saveFetchResult(data: T)
open fun onFetchFailed(exception: Exception) = Unit
open fun shouldFetch(data: T) = true
}
Like #CommonsWare said in the comments, however, it'd be nicer to just expose a Flow<T>. Here's what I've tried coming up with to do that. Note that I haven't used this code in production, so buyer beware.
import kotlinx.coroutines.flow.*
import nihk.core.Resource
abstract class NetworkBoundResource<T> {
fun asFlow(): Flow<Resource<T>> = flow {
val flow = query()
.onStart { emit(Resource.Loading<T>(null)) }
.flatMapConcat { data ->
if (shouldFetch(data)) {
emit(Resource.Loading(data))
try {
saveFetchResult(fetch())
query().map { Resource.Success(it) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
query().map { Resource.Error(throwable, it) }
}
} else {
query().map { Resource.Success(it) }
}
}
emitAll(flow)
}
abstract fun query(): Flow<T>
abstract suspend fun fetch(): T
abstract suspend fun saveFetchResult(data: T)
open fun onFetchFailed(throwable: Throwable) = Unit
open fun shouldFetch(data: T) = true
}
#N1hk answer works right, this is just a different implementation that doesn't use the flatMapConcat operator (it is marked as FlowPreview at this moment)
#FlowPreview
#ExperimentalCoroutinesApi
abstract class NetworkBoundResource<ResultType, RequestType> {
fun asFlow() = flow {
emit(Resource.loading(null))
val dbValue = loadFromDb().first()
if (shouldFetch(dbValue)) {
emit(Resource.loading(dbValue))
when (val apiResponse = fetchFromNetwork()) {
is ApiSuccessResponse -> {
saveNetworkResult(processResponse(apiResponse))
emitAll(loadFromDb().map { Resource.success(it) })
}
is ApiErrorResponse -> {
onFetchFailed()
emitAll(loadFromDb().map { Resource.error(apiResponse.errorMessage, it) })
}
}
} else {
emitAll(loadFromDb().map { Resource.success(it) })
}
}
protected open fun onFetchFailed() {
// Implement in sub-classes to handle errors
}
#WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
#WorkerThread
protected abstract suspend fun saveNetworkResult(item: RequestType)
#MainThread
protected abstract fun shouldFetch(data: ResultType?): Boolean
#MainThread
protected abstract fun loadFromDb(): Flow<ResultType>
#MainThread
protected abstract suspend fun fetchFromNetwork(): ApiResponse<RequestType>
}
I am new to Kotlin Coroutine. I just come across this problem this week.
I think if you go with the repository pattern as mentioned in the post above, my opinion is feeling free to pass a CoroutineScope into the NetworkBoundResource. The CoroutineScope can be one of the parameters of the function in the Repository, which returns a LiveData, like:
suspend fun getData(scope: CoroutineScope): LiveDate<T>
Pass the build-in scope viewmodelscope as the CoroutineScope when calling getData() in your ViewModel, so NetworkBoundResource will work within the viewmodelscope and be bound with the Viewmodel's lifecycle. The coroutine in the NetworkBoundResource will be cancelled when ViewModel is dead, which would be a benefit.
To use the build-in scope viewmodelscope, don't forget add below in your build.gradle.
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01'

Categories

Resources