Reputation: 1456
I want to use reactive paradigm using Kotlin Flow
in my Android project. I have an external callback-based API so my choice is using callbackFlow
in my Repository
class.
I've already read insightfully some proper docs with no help:
What I want to achieve:
Currently my Repository
class looks like this (simplified code):
lateinit var callback: ApiCallback
fun someFlow() = callbackFlow<SomeModel> {
callback = object : ApiCallback {
override fun someApiMethod() {
offer(SomeModel())
}
}
awaitClose { Log.d("Suspending flow until methods aren't invoked") }
}
suspend fun someUnfortunateCallbackDependentCall() {
externalApiClient.externalMethod(callback)
}
Problem occurs when someUnfortunateCallbackDependentCall
is invoked faster than collecting someFlow()
.
For now to avoid UninitializedPropertyAccessException
I added some delays in my coroutines before invoking someUnfortunateCallbackDependentCall
but it is kind of hack/code smell for me.
My first idea was to use by lazy
instead of lateinit var
as this is what I want - lazy initialization of callback object. However, I couldn't manage to code it altogether. I want to emit/offer/send some data from someApiMethod
to make a data flow but going outside of callbackFlow
would require ProducerScope
that is in it. And on the other hand, someUnfortunateCallbackDependentCall
is not Kotlin Flow-based at all (could be suspended using Coroutines
API at best).
Is it possible to do? Maybe using some others Kotlin delegates? Any help would be appreciated.
Upvotes: 2
Views: 4951
Reputation: 1457
To answer your question technically, you can of course intialise a callback lazyily or with lateinit, but you can't do this AND share the coroutine scope (one for the Flow and one for the suspend function) at the same time - you need to build some kind of synchronisation yourself.
Below I've made some assumptions about what you are trying to achieve, perhaps they are not perfect for you, but hopefully give some incite into how to improve.
Since it is a Repository that you are creating, I will first assume that you are looking to store SomeModel
and allow the rest of your app to observe changes to it. If so, the easiest way to do this is with a MutableStateFlow
property instead of a callbackFlow
:
interface Repository {
val state: Flow<SomeModel>
suspend fun reload()
}
class RepositoryImpl(private val service: ApiService) : Repository {
override val state = MutableStateFlow(SomeModel())
override suspend fun reload() {
return suspendCoroutine { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
state.value = data
if (continuation.context.isActive)
continuation.resume(Unit)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
The downside to this solution is that you have to call reload()
in order to actually make a call to your backend, collecting the Flow alone is not enough.
myrepository.state.collect {}
myrepository.reload()
Another solution, again depending on what exactly you are trying to achieve, is to provide two ways to call your backend:
interface Repository {
fun someFlow(): Flow<SomeModel>
suspend fun reload(): SomeModel
}
class RepositoryImpl(private val service: ApiService) : Repository {
override fun someFlow() = callbackFlow<SomeModel> {
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
offer(data)
}
})
awaitClose {
Log.d("TAG", "Callback Flow is closed")
}
}
override suspend fun reload(): SomeModel {
return suspendCoroutine<SomeModel> { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
if (continuation.context.isActive)
continuation.resume(data)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
Now you can either call reload()
or someFlow()
to retrieve SomeModel()
and the Repository
holds no "state".
Note that the reload()
function is simply a 'coroutine' version of the callbackFlow
idea.
Upvotes: 2