Kotlin reflection with coroutines - android

I need to call the method suspend fun insert(e: T) by reflection declared as follows:
interface IMutableDao<T> {
#Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(e: T)
#Update
suspend fun update(e: T)
#Delete
suspend fun delete(e: T)
}
I tried with:
val insertFun = IMutableDao::class.functions.find {
it.name.equals("insert", true)
}
insertFun!!.callSuspend(dao, o)
But I get the exception "Callable expects 3 arguments, but 2 were provided.". I do not understand where the 3rd argument comes from.
UPDATE
I have found the problem. The 3rd is a Continuation instance. Does anyone know what to pass there ? I couldn't find any suitable instance.
UPDATE 2
The workaround I found was to create a temporary class inside the function I call the suspended function from, like this:
val c = object : Continuation<Unit> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
}
}

You are missing the "this" parameter which is the object that the method should be called with respect to.
it should be the first argument to the method.

You may use suspendCoroutine with Unit as type and use the continuation instance.
GlobalScope.launch {
suspendCoroutine<Unit> { continuation ->
insertFun!!.callSuspend(dao, o, continuation)
}
}

Related

How to return value in coroutine scope?

Is it possible to to return value in Coroutine Scope without run blocking?
For now my code in repository looks like this:
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? {
runBlocking {
return#runBlocking
CoroutineScope(Dispatchers.Main).launch {
getWorkItemByIdUseCase.build(workItemId)
}
}
return null
}
this is my useCase
class GetWorkItemByIdUseCase(private val workItemDao: WorkItemDao) :
BaseUseCase<Int, WorkItemRoom>() {
override suspend fun create(id: Int): WorkItemRoom {
return workItemDao.getWorkItemById(id)
}
}
baseUseCase
abstract class BaseUseCase<P, R> {
protected abstract suspend fun create(params: P): R
open suspend fun build(params: P): R = create(params)
}
Dao
#Dao
abstract class WorkItemDao {
#Query("SELECT * FROM workitem WHERE id=:id")
abstract suspend fun getWorkItemById(id: Int): WorkItemRoom
}
... but certainly I know it is not a proper solution. How would you achieve this? In viewmodels' or fragments I can directly use lifecycleScope`, but what in other cases, where the must is to call useCase directly from method below. Is it efficient to call Dispatchers.Main all the time?
CoroutineScope(Dispatchers.Main).launch { }
You could just pass the value from a coroutine to liveData and then use an observer
private val observableMutableLiveData = MutableLiveData<Type>()
val observableLiveData: LiveData<Type> = observableMutableLiveData
and later in a coroutine:
CoroutineScope(Dispatchers.Main).launch {
observableMutableLiveData.postValue(value)
}
and then in an activity:
viewModel.observableLiveData.observe(this) { Type ->
Log.i("result", Type)
}
It doesn't make sense to use runBlocking in a suspend function. (It hardly ever makes sense to use it at all, except as a bridge between coroutines and non-coroutine code in a project that is partially converted to using coroutines but still needs to support legacy code or libraries.)
You should just call the function you need.
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? { //may not need nullable return value
return getWorkItemByIdUseCase.build(workItemId)
}
If you need to specify a dispatcher, use withContext:
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? = withContext(Dispatchers.Main) {
getWorkItemByIdUseCase.build(workItemId)
}
However, if build is a suspend function, there's no need to specify a Dispatcher when calling it. Suspend functions are responsible for internally calling their functions on appropriate threads/dispatchers.
If you need a coroutine scope inside a coroutine or suspend function, use the lowercase coroutineScope function, which creates a scope that will be automatically cancelled if the coroutine is cancelled. This example doesn't make much sense, because normally you don't need a new scope unless you are running parallel jobs inside it:
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? = coroutineScope(Dispatchers.Main) {
getWorkItemByIdUseCase.build(workItemId)
}
Have you ever listened about a lambda?
It looks like call: (MyResult) -> Unit
I use it from time to time like
fun someToDo(call: (MyResult) -> Unit) {
scope.launch(Dispatchers.IO) {
val result = getFromSomeWere()
launch(Dispatchers.Main){call(result)}
}
}

Kotlin Flow: callbackFlow with lazy initializer of callback object

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:
callbackFlow documentation
Callbacks and Kotlin Flows by Roman Elizarov
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.
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.

Run suspend function inside retrofit onResponse method

I can receive data from network by retrofit.this code is hosted in repository class:
fun getQuestionsFromWebServer() {
val webService = AppModule()
val call = webService.provideModirfaniService().getQuestions()
call.enqueue(object : Callback<List<Question>> {
override fun onResponse(
call: Call<List<Question>>,
response: Response<List<Question>>
) {
Log.d("amin", response.body().toString())
val statusCode = response.code()
val questionList= response.body()
if(response.isSuccessful) {
if (questionList != null) {
//This is a suspend function
questionDao.updateQuestions(questionList)
}
}
}
override fun onFailure(call: Call<List<Question>>, t: Throwable) {
Log.d("amin", "failed to get questions from network")
}
})
}
Data should be inserted/updated to DB after receive. update method in DO:
#Update
suspend fun updateQuestions(list: List<Question>)
If the code was in my viewModel I could run suspend function inside viewModelScope.launch{} ,I need to know how to run a suspending function inside Retrofit onResponse method when it is in repository?
(I have not access to viewModel or Activity/Fragment instance inside repository class )
GlobalScope.launch seems to be the best fit, since you dont have access to any LifeCycleAware component. from the docs :
Global scope is used to launch top-level coroutines which are
operating on the whole application lifetime and are not cancelled
prematurely.
Please note that any coroutines launched with viewModelScope will get canceled when ViewModel is cleared, but no such cancellation happens with GlobalScope coroutines.
if that data is going to be stored for using somewhere else, you might use CoroutineScope. If data is reload and used only in the same fragment, and is no shared, use lifeCycleScope.

Is it possible to spy on suspend Android Room DAO functions with MockK

I am investigation the MockK library with my Android JUnit tests
testImplementation "io.mockk:mockk:1.10.0"
I have an issue when attempting to spyk on suspend functions
heres my Junit test
#ExperimentalCoroutinesApi
#FlowPreview
#RunWith(AndroidJUnit4::class)
class BackOffCriteriaDaoTest : BaseTest() {
#Rule
#JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var dao: BackoffCriteriaDAO
#Test
fun backOffCriteria() = runBlocking {
dao = spyk(myRoomDatabase.backoffCriteriaDAO())
assertNotNull(dao.getBackoffCriteria())
assertEquals(backOffCriteriaDO, dao.getBackoffCriteria())
dao.delete()
coVerify {
myRoomDatabase.backoffCriteriaDAO()
dao.reset()
}
}
}
This test throws an java.lang.AssertionError at dao.reset() as follows:-
java.lang.AssertionError: Verification failed: call 2 of 2: BackoffCriteriaDAO_Impl(#2).reset(eq(continuation {}))). Only one matching call to BackoffCriteriaDAO_Impl(#2)/reset(Continuation) happened, but arguments are not matching:
[0]: argument: continuation {}, matcher: eq(continuation {}), result: -
My dao reset() method resembles this:-
#Transaction
suspend fun reset() {
delete()
insert(BackoffCriteriaDO(THE_BACKOFF_CRITERIA_ID, BACKOFF_CRITERIA_MILLISECOND_DELAY, BACKOFF_CRITERIA_MAX_RETRY_COUNT))
}
Why am I seeing this java.lang.AssertionError?
How do I coVerify that suspend functions have been called?
UPDATE
I believe the issue is caused by the fact I am using Room database.
My dao interface method reset() is implemented by room generated code as
#Override
public Object reset(final Continuation<? super Unit> p0) {
return RoomDatabaseKt.withTransaction(__db, new Function1<Continuation<? super Unit>, Object>() {
#Override
public Object invoke(Continuation<? super Unit> __cont) {
return BackoffCriteriaDAO.DefaultImpls.reset(BackoffCriteriaDAO_Impl.this, __cont);
}
}, p0);
}
which means the coVerify{} is matching this function and not my interface version.
Is it possible to match this generated version of public Object reset(final Continuation<? super Unit> p0)?
Is this a more basic issue with mockk that it cannot mockk java classes?
Or Java implementations of Kotlin interfaces?
UPDATE 2
When my Room DAO functions are not suspend then Mockk works as required
using these dummy functions in my DAO:-
#Transaction
fun experimentation() {
experiment()
}
#Transaction
fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
My test passes
#Test
fun experimentation() = runBlocking {
val actual = myRoomDatabase.backoffCriteriaDAO()
val dao = spyk(actual)
dao.experimentation()
verify { dao.experiment() }
}
When I change my dummy functions as follows the test still passes
#Transaction
suspend fun experimentation() {
experiment()
}
#Transaction
fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
However when I change my dummy functions as follows the test throws an exception
#Transaction
suspend fun experimentation() {
experiment()
}
#Transaction
suspend fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
The failing tests resembles this:-
#Test
fun experimentation() = runBlocking {
val actual = myRoomDatabase.backoffCriteriaDAO()
val dao = spyk(actual)
dao.experimentation()
coVerify { dao.experiment() }
}
The exception is
java.lang.AssertionError: Verification failed: call 1 of 1: BackoffCriteriaDAO_Impl(#2).experiment(eq(continuation {}))). Only one matching call to BackoffCriteriaDAO_Impl(#2)/experiment(Continuation) happened, but arguments are not matching:
[0]: argument: continuation {}, matcher: eq(continuation {}), result: -
There might be nothing wrong with spy but the asynchronous nature of the transaction function you are invoking.
To test with suspending functions with scope you might need to use
launch builder and advance time until idle, or for a time period for testing the progress, like it's done with RxJava counter parts.
I had the same issue with MockWebServer, here you can check out the question.
launch {
dao.delete()
}
advanceUntilIdle()
And use Coroutine rule with tests to have same scope for each operation.
class TestCoroutineRule : TestRule {
private val testCoroutineDispatcher = TestCoroutineDispatcher()
val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
#Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest { block() }
}
You can use rule as below
testCoroutineRule.runBlockingTest {
dao.delete()
advanceUntilIdle()
coVerify {
myRoomDatabase.backoffCriteriaDAO()
dao.reset()
}
}
Also you can try putting dao.delete() in launch. In some tests it did not work without launch while some other work without it and even some of them are flakky with everything i tried. There are some issues with coroutines-test to be solved.
here you can check how it's done and there are some issues with test-coroutines, you can check out my other question here.
I created a playground to test coroutines, it might helpful and you can test out the issues with coroutines, and another one with mockK and coroutines tests.

Kotlin: Is it possible to pass two 'this' in a function?

With the below code I'm getting the following error: "Suspend function 'getSomethingFromAPI' should be called only from a coroutine or another suspend function.", which is current. getSomethingFromAPI is indeed a suspend function of the ViewModel.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(CallVM::class.java)
viewModel.applyLaunch {
this.getSomethingFromAPI()
}
}
fun <T: ViewModel> T.applyLaunch(block: T.() -> Unit)
= apply { viewModelScope.launch(Dispatchers.IO) { block() } }
As you can see though, in the applelaunch function I am executing getSomethingFromAPI inside of a coroutine (launch), but this information is lost. Is there any way to preserve it and keep T as ViewModel at the same time?
To be more specific, is it possible to have a shortcut function that implements two first lines of the below code?
viewModel.apply {
viewModelScope.launch(Dispatchers.IO) {
getSomethingFromAPI()
}
getSomethingFromAPI above sees both 'this' (ViewModel and coroutine).
I know it's not something important to have, but it might be good to know for creating DSL.
You are getting this error because you are trying to call a suspend function in a non-suspend lambda. Make lambda in applyLaunch suspend block: suspend T.() -> Unit

Categories

Resources