I am implementing the coroutine for first time. I am following MVP pattern for a simple login app. Here is my code flow -
The login button clicked will follow this direction -
LoginFragment -> LoginPresenter -> Repository -> APIRepository -> RetrofitInterface
The login response will follow this direction -
RetrofitInterface -> APIRepository -> Repository -> LoginPresenter -> LoginFragment
Here is the code -
RetrofitInterface.kt
#POST("login")
fun loginAPI(#Body loginRequest: LoginRequest): Deferred<LoginResponse>?
Here is my Result.kt
sealed class Result<out T : Any> {
class Success<out T : Any>(val data: T) : Result<T>()
class Error(val exception: Throwable, val message: String = exception.localizedMessage) : Result<Nothing>()
}
APIRepository.kt
override suspend fun loginAPICall(loginRequest: LoginRequest) : Result<LoginResponse>? {
try {
val loginResponse = apiInterface?.loginAPI(loginRequest)?.await()
return Result.Success<LoginResponse>(loginResponse!!)
} catch (e : HttpException) {
return Result.Error(e)
} catch (e : Throwable) {
return Result.Error(e)
}
}
Repository.kt
override suspend fun loginUser(loginRequest: LoginRequest): Result<LoginResponse> {
if (isInternetPresent(context)) {
val result = apiRepositoryInterface?.loginAPICall(loginRequest)
if (result is Result.Success<LoginResponse>) {
val loginData = result.data
cache?.storeData(loginData)
}
return result!!
} else {
return Result.Error(Exception())
}
}
How do I launch a coroutine now in my presenter? I need to execute this API call on a background thread and publish the results on UI thread?
You need to launch a coroutine in a Presenter using local scope and injected CoroutineContext to be able to change it, for example in Unit Tests:
class Presenter(
private val repo: Repository,
private val uiContext: CoroutineContext = Dispatchers.Main
) : CoroutineScope { // creating local scope
private var job: Job = Job() // or SupervisorJob() - children of a supervisor job can fail independently of each other
// To use Dispatchers.Main (CoroutineDispatcher - runs and schedules coroutines)
// in Android add dependency: implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun detachView() {
// cancel the job when view is detached
job.cancel()
}
fun login(request: LoginRequest) = launch { // launching a coroutine
val result = repo.loginUser(request) // calling 'loginUser' function will not block the Main Thread, it suspends the coroutine
//use result, update UI
when (result) {
is Success<LoginResponse> -> { /* update UI when login success */ }
is Error -> { /* update UI when login error */ }
}
}
}
you can use coroutine in this way
private var parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Main
private val scope = CoroutineScope(coroutineContext)
scope.launch(Dispatchers.IO) {
// your api call
}
you can call parentJob.cancel() to cancel the job or call it in onClear of ViewModel
Related
How to initiate work flow or start chain of actions in java/kotlin?
I have a workflow which has severals steps. Each step is executed based on if conditions, if the condition satisfied we execute the step and if not satisfied move on to next step.
Step1: create an interface
interface Chain {
fun setNextChain(nextChain: Chain?)
fun getChainResponse(someInput: SomeInputClass): ChainResponse?
}
Step2: create classes for ech steps in your workflow and implement the chain interface
class Step1InWorkflow : Chain {
private var nextInChain: Chain? = null
override fun setNextChain(nextChain: Chain?) {
nextInChain = nextChain
}
override fun getChainResponse(
someInput: SomeInputClass
): ChainResponse? {
return if ( execute step1 based on some condition) {
// FLOW -> Step 1
getStepOneResponse(contractant)
} else {
setNextChain(Step2InWorkflow())
nextInChain?.getChainResponse(someInput: SomeInputClass)
}
}
private fun getStepOneResponse(input: Input): ChainResponse {
// logic and return response
}
}
Step3: End of workflow
class Step2InWorkflow : Chain {
private var nextInChain: Chain? = null
override fun setNextChain(nextChain: Chain?) {
nextInChain = nextChain
}
override fun getChainResponse(someInput: SomeInputClass): ChainResponse? {
// some business logic
return getStep2Response(input)
}
private fun getStep2Response(input:Input): ChainResponse {
// return response
}
}
I'm getting below crash only on samsung J7(Android 8) device, when I run coroutine inside workmanager.
Fatal Exception: java.lang.IncompatibleClassChangeError: Class 'com.example.myapplication.SyncAndSaveWork' does not implement interface 'kotlin.d.f' in call to 'java.lang.Object kotlin.d.f.fold(java.lang.Object, kotlin.g.a.m)' (declaration of 'kotlinx.coroutines.internal.y' appears in base.apk)
at kotlinx.coroutines.internal.ThreadContextKt.threadContextElements(ThreadContextKt.java:60)
at kotlinx.coroutines.DispatchedContinuation.<init>(DispatchedContinuation.java:29)
at kotlinx.coroutines.CoroutineDispatcher.interceptContinuation(CoroutineDispatcher.java:99)
at kotlin.coroutines.jvm.internal.ContinuationImpl.intercepted(ContinuationImpl.java:112)
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsKt__IntrinsicsJvmKt.java:137)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(CancellableKt.java:26)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.java:109)
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.java:158)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(BuildersKt__Builders_commonKt.java:54)
at kotlinx.coroutines.BuildersKt.launch(BuildersKt.java:1)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(BuildersKt__Builders_commonKt.java:47)
at kotlinx.coroutines.BuildersKt.launch$default(BuildersKt.java:1)
at androidx.work.Worker$1.run(Worker.java:85)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
My Worker class:
class SyncAndSaveWork(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
#Volatile
lateinit var latch: CountDownLatch
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
Log.d("SyncAndSaveWork", "$exception handled !")
latch.countDown()
}
override fun doWork(): Result {
latch = CountDownLatch(1)
syncData()
latch.await()
Log.d("SyncAndSaveWork", "Finished")
// Indicate whether the work finished successfully with the Result
print("After task")
return Result.success()
}
private fun syncData() { // error at this line I guess
Globle.launch(Dispatchers.Main){
performTask()
Log.d("SyncAndSaveWork", "Done")
}
}
private fun performTask() {
Global.launch(Dispatchers.IO + exceptionHandler) {
delay(2000) // implementation excluded for bravity
print("Task compeleted")
Log.d("SyncAndSaveWork", "Compeleted")
latch.countDown()
}
}
}
Configuration and scheduling workmanger inside Application class:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
var constraints = with(Constraints.Builder()) {
setRequiredNetworkType(NetworkType.CONNECTED)
}.build()
var request = with(OneTimeWorkRequest.Builder(SyncAndSaveWork::class.java)) {
setConstraints(constraints)
addTag("SyncAndSaveWork")
setInitialDelay(4, TimeUnit.SECONDS)
setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
}.build()
WorkManager.getInstance().cancelAllWork()
WorkManager.getInstance().enqueue(request)
}
}
I don't understand why it's happening only on certain devices, am I doing something wrong? Please help with this, thanks in advance.
I am new in coroutines. So now I look how to use coroutines instead of handlers
The Handler code:
fun Handler.repostDelayed(func: Runnable, delay: Long) {
removeCallbacksAndMessages(null)
postDelayed(func, delay)
}
Analog in Coroutines
inline fun AppCompatActivity.repostDelayed(crossinline func: () -> Unit, delay: Long) {
lifecycleScope.cancel()
lifecycleScope.launch {
delay(delay) //debounce timeOut
func()
}
}
But it does not work.
Could You fix my expression for Coroutines, please?
So, I have found the solution here.
And have just modified a little:
fun <T, V> CoroutineScope.debounce(
waitMs: Long = 300L,
destinationFunction: T.(V) -> Unit
): T.(V) -> Unit {
var debounceJob: Job? = null
return { param: V ->
debounceJob?.cancel()
debounceJob = launch {
delay(waitMs)
destinationFunction(param)
}
}
}
usage:
private val delayFun: String.(Boolean) -> Unit = lifecycleScope.debounce(START_DELAY) {
if(it){
print(this)
}
}
//call function
"Hello world!".delayFun(true)
The benefit of coroutine usage is you don't need to cancel coroutine when view onDesstroy because it works automatically!
But for the handler, you must call removeCallbacksAndMessages onDestroy
Let's say we have the following function to test
fun loadData(dataId: Long, completion: (JsonElement?, Exception?) -> Unit) {
underlayingApi.post(url = "some/rest/url",
completion = { rawResult, exception ->
val processedResult = processJson(rawResult)
completion(processedResult, exception)
})
}
It's clear to me how to mock, inject, stub and verify the calls to underlayingApi.
How to verify the result returned via completion(processedResult, exception)?
To test the lambdas behavior, the underlayingApi has to be mocked where the lambda is invoked via the InvoactionOnMock object like this.
`when`(underlayingApi.post(eq("some/rest/url"),
any())).thenAnswer {
val argument = it.arguments[1]
val completion = argument as ((rawResult: String?, exception: Exception?) -> Unit)
completion.invoke("result", null)
}
This leads to the invocation of the callback within the object under test. Now to check if the callback out of the object under test is working verify it like that.
objUnderTest.loadData(id,
{ json, exception ->
assert....
})
Building on Martin's answer, here is my approach without lint warnings:
import com.nhaarman.mockito_kotlin.*
#Test
fun loadData() {
val mockUnderlyingApi: UnderlayingApi = mock()
val underTest = ClassBeingTested()
underTest.underlayingApi = mockUnderlyingApi
whenever(mockUnderlyingApi.post(eq("some/rest/url"), any())).thenAnswer {
val completion = it.getArgument<((rawResult: String?, exception: Exception?) -> Unit)>(1)
completion.invoke("result", null)
}
underTest.loadData(0L,
{ jsonElement, exception ->
// Check whatever you need to check
// on jsonElement an/or exception
})
}
I have a simple view with button that starts Intent.ACTION_PICK for result and than displays chosen contact on screen. To do that following steps must be taken:
check if android.permission.READ_CONTACTS is granted
open contact activity
select contact and go back to app
check for android.permission.READ_CONTACTS again
find contact by given uri
show contact on screen
I want to test scenario when one open contacts than revokes permission and goes back to app with selected contact. Expected result is not to call method that find contacts by its uri.
Unfortunately current implementation throws:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
for:
whenever(interactor.getContact(any())).thenReturn(Maybe.just(Contact()).doOnSuccess { find = true })
I know that I can replace StrictStubs with Silent but I'm looking for better solution with refactoring current code.
All necessary class and test:
class Contact
interface View {
val contactClicks: Observable<Any>
fun setContact(contact: Contact)
}
interface Interactor {
fun getContact(uri: String): Maybe<Contact>
}
interface Router {
fun goToContacts(): Maybe<String>
}
interface Permissioner {
fun requestReadContacts(): Single<Boolean>
}
class Presenter(
private val view: View,
private val interactor: Interactor,
private val router: Router,
private val permissioner: Permissioner
) {
private val disposables: CompositeDisposable = CompositeDisposable()
fun bindView() {
view.contactClicks
.flatMapSingle { permissioner.requestReadContacts() } //ask first time before opening contacts
.filter { it }
.flatMapMaybe { router.goToContacts() }
.flatMapMaybe {
permissioner.requestReadContacts() //ask second time before using ContentResolver
.filter { granted -> granted }
.flatMap { _ -> interactor.getContact(it) }
}
.subscribeBy { view.setContact(it) }
.addTo(disposables)
}
}
#RunWith(MockitoJUnitRunner.StrictStubs::class)
class PresenterTest {
#Mock
lateinit var view: View
#Mock
lateinit var router: Router
#Mock
lateinit var permissioner: Permissioner
#Mock
lateinit var interactor: Interactor
#InjectMocks
lateinit var presenter: Presenter
private val contactClickSubject = PublishSubject.create<Any>()
#Before
fun setUp() {
whenever(view.contactClicks).thenReturn(contactClickSubject)
}
#Test
fun shouldNotFindContactWhenReturnedWithUriAndPermissionNotGrantedSecondTime() {
var firstTimeAsk = true
whenever(permissioner.requestReadContacts()).thenReturn(Single.fromCallable {
if (firstTimeAsk) {
firstTimeAsk = false
return#fromCallable true
} else {
return#fromCallable false
}
})
whenever(router.goToContacts()).thenReturn(Maybe.just("contact"))
var find = false
whenever(interactor.getContact(any())).thenReturn(Maybe.just(Contact()).doOnSuccess { find = true })
presenter.bindView()
contactClickSubject.onNext(Any())
assertFalse(find)
}
}
UnnecessaryStubbingException means you are stubbing something, but not really using it. And that's correct, in your case interactor.getContact should be never called in test - this is desired behaviour. So there is no point in stubbing it.
The simplest solution would be to remove unnecessary variable var find = false and stubbing - substitute them with assertion at the end of your test:
verify(interactor, never()).getContact(any())
This is equivalent to your current solution but more straightforward than using helper variables.