I am new to coroutines. SO I just wanted to know what is the best way to use them.
My scenraio/use case is I want to make a API call on IO thread and observe the results on Main thread and update the UI. Also when fragment's onDestoryView() is called, then I want to cancel my job.
My fragment asks the presenter for some updates. So my presenter has a coroutine running like this -
class MyPresenter(view: MyView,
private val coroutineCtx: CoroutineContext = Dispatchers.Main) : CoroutineScope {
private val job: Job = Job()
private var view: MyView? = null
init {
this.view= view
}
override val coroutineContext: CoroutineContext
get() = job + coroutineCtx
fun updateData() = launch{
//repo is singleton
val scanResult = repo.updateData()
when(scanResult) {
sucess -> { this.view.showSuccess()}
}
}
fun stopUpdate() {
job.cancel()
}
}
In my repository,
suspend fun updateData(): Result<Void> {
val response = API.update().await()
return response
}
Am I using coroutines correctly? If yes, my job.cancel() never seems to work although I call it from fragment's onDestroyView().
From my point of view you are using coroutine correctly. A few notes:
You don't have to pass view: MyView to the constructor, and assign its value to the property in init block. Instead you can mark view parameter in the constructor as val and it will became a property:
class MyPresenter(private val view: MyView,
private val coroutineCtx: CoroutineContext = Dispatchers.Main) : CoroutineScope {
// you can get rid of the next lines:
private var view: MyView? = null
init {
this.view= view
}
}
launch function returns a Job. You can add an extension function, e.g. launchSilent, to return Unit :
fun CoroutineScope.launchSilent(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) {
launch(context, start, block)
}
From my observation job.cancel() works correctly: when you invoke it a coroutine must stop. For example if we put some logs:
fun updateData() = launch{
Log.d("Tag", "launch start")
val scanResult = repo.updateData()
when(scanResult) {
success -> { this.view.showSuccess()}
}
Log.d("Tag", "launch end")
}
And add some delay to the repo's updateData() function:
suspend fun updateData(): Result<Void> {
delay(5000)
val response = API.update().await()
return response
}
And, for example, in the fragment after invoking presenter.updateData() we call something like Handler().postDelayed({ presenter.stopUpdate() }, 3000) we won't see "launch end" log in the Logcat.
Related
I have a Repository defined as the following.
class StoryRepository {
private val firestore = Firebase.firestore
suspend fun fetchStories(): QuerySnapshot? {
return try {
firestore
.collection("stories")
.get()
.await()
} catch(e: Exception) {
Log.e("StoryRepository", "Error in fetching Firestore stories: $e")
null
}
}
}
I also have a ViewModel like this.
class HomeViewModel(
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val storyRepository = StoryRepository()
private var _stories = MutableLiveData<List<Story>>()
val stories: LiveData<List<Story>>
get() = _stories
init {
uiScope.launch {
getStories()
}
uiScope.launch {
getMetadata()
}
}
private suspend fun getStories() {
withContext(Dispatchers.IO) {
val snapshots = storyRepository.fetchStories()
// Is this correct?
if (snapshots == null) {
cancel(CancellationException("Task is null; local DB not refreshed"))
return#withContext
}
val networkStories = snapshots.toObjects(NetworkStory::class.java)
val stories = NetworkStoryContainer(networkStories).asDomainModel()
_stories.postValue(stories)
}
}
suspend fun getMetadata() {
// Does some other fetching
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
As you can see, sometimes, StoryRepository().fetchStories() may fail and return null. If the return value is null, I would like to not continue what follows after the checking for snapshots being null block. Therefore, I would like to cancel that particular coroutine (the one that runs getStories() without cancelling the other coroutine (the one that runs getMetadata()). How do I achieve this and is return-ing from withContext a bad-practice?
Although your approach is right, you can always make some improvements to make it simpler or more idiomatic (especially when you're not pleased with your own code).
These are just some suggestions that you may want to take into account:
You can make use of Kotlin Scope Functions, or more specifically the let function like this:
private suspend fun getStories() = withContext(Dispatchers.IO) {
storyRepository.fetchStories()?.let { snapshots ->
val networkStories = snapshots.toObjects(NetworkStory::class.java)
NetworkStoryContainer(networkStories).asDomainModel()
} ?: throw CancellationException("Task is null; local DB not refreshed")
}
This way you'll be returning your data or throwing a CancellationException if null.
When you're working with coroutines inside a ViewModel you have a CoroutineScope ready to be used if you add this dependendy to your gradle file:
androidx.lifecycle:lifecycle-viewmodel-ktx:{version}
So you can use viewModelScope to build your coroutines, which will run on the main thread:
init {
viewModelScope.launch {
_stories.value = getStories()
}
viewModelScope.launch {
getMetadata()
}
}
You can forget about cancelling its Job during onCleared since viewModelScope is lifecycle-aware.
Now all you have left to do is handling the exception with a try-catch block or with the invokeOnCompletion function applied on the Job returned by the launch builder.
I've got a broadcastReceiver that starts a coroutine and I am trying to unit test that...
The broadcast:
class AlarmBroadcastReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Timber.d("Starting alarm from broadcast receiver")
//inject(context) Don't worry about this, it's mocked out
GlobalScope.launch {
val alarm = getAlarm(intent)
startTriggerActivity(alarm, context)
}
}
private suspend fun getAlarm(intent: Intent?): Alarm {
val alarmId = intent?.getIntExtra(AndroidAlarmService.ALARM_ID_KEY, -1)
if (alarmId == null || alarmId < 0) {
throw RuntimeException("Cannot start an alarm with an invalid ID.")
}
return withContext(Dispatchers.IO) {
alarmRepository.getAlarmById(alarmId)
}
}
And here's the test:
#Test
fun onReceive_ValidAlarm_StartsTriggerActivity() {
val alarm = Alarm().apply { id = 100 }
val intent: Intent = mock {
on { getIntExtra(any(), any()) }.thenReturn(alarm.id)
}
whenever(alarmRepository.getAlarmById(alarm.id)).thenReturn(alarm)
alarmBroadcastReceiver.onReceive(context, intent)
verify(context).startActivity(any())
}
What's happening is that the function I'm verifying is never being called. The test ends before the coroutine returns... I'm aware that GlobalScope is bad to use, but I'm not sure how else to do it.
EDIT 1:
If I put a delay before the verify, it seems to work, as it allows time for the coroutine to finish and return, however, I don't want to have test relying on delay/sleep... I think the solution is to properly introduce a scope instead of using GlobalScope and control that in the test. Alas, I have no clue what is the convention for declaring coroutine scopes.
I see, You will have to use an Unconfined dispatcher:
val Unconfined: CoroutineDispatcher (source)
A coroutine dispatcher that is not confined to any specific thread. It executes the initial continuation of a coroutine in the current call-frame and lets the coroutine resume in whatever thread that is used by the corresponding suspending function, without mandating any specific threading policy. Nested coroutines launched in this dispatcher form an event-loop to avoid stack overflows.
Documentation sample:
withContext(Dispatcher.Unconfined) {
println(1)
withContext(Dispatcher.Unconfined) { // Nested unconfined
println(2)
}
println(3)
}
println("Done")
For my ViewModel tests, I pass a coroutine context to the ViewModel constructor so that I can switch between Unconfined and other dispatchers e.g. Dispatchers.Main and Dispatchers.IO.
Coroutine context for tests:
#ExperimentalCoroutinesApi
class TestContextProvider : CoroutineContextProvider() {
override val Main: CoroutineContext = Unconfined
override val IO: CoroutineContext = Unconfined
}
Coroutine context for the actual ViewModel implementation:
open class CoroutineContextProvider {
open val Main: CoroutineContext by lazy { Dispatchers.Main }
open val IO: CoroutineContext by lazy { Dispatchers.IO }
}
ViewModel:
#OpenForTesting
class SampleViewModel #Inject constructor(
val coroutineContextProvider: CoroutineContextProvider
) : ViewModel(), CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext = job + coroutineContextProvider.Main
override fun onCleared() = job.cancel()
fun fetchData() {
launch {
val response = withContext(coroutineContextProvider.IO) {
repository.fetchData()
}
}
}
}
Update
As of coroutine-core version 1.2.1 you can use runBlockingTest:
Dependencies:
def coroutines_version = "1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
e.g:
#Test
fun `sendViewState() sends displayError`(): Unit = runBlockingTest {
Dispatchers.setMain(Dispatchers.Unconfined)
val apiResponse = ApiResponse.success(data)
whenever(repository.fetchData()).thenReturn(apiResponse)
viewModel.viewState.observeForever(observer)
viewModel.processData()
verify(observer).onChanged(expectedViewStateSubmitError)
}
Yeah as Rodrigo Queiroz mentioned, run blocking will solve the issue.
#Test
fun onReceive_ValidAlarm_StartsTriggerActivity() = runBlockingTest {
val alarm = Alarm().apply { id = 100 }
val intent: Intent = mock {
on { getIntExtra(any(), any()) }.thenReturn(alarm.id)
}
whenever(alarmRepository.getAlarmById(alarm.id)).thenReturn(alarm)
alarmBroadcastReceiver.onReceive(context, intent)
verify(context).startActivity(any())
}
I have two coroutines tests that both pass when run individually, but if I run them together the second one always fails (even if I switch them around!). The error I get is:
Wanted but not invoked: observer.onChanged([SomeObject(someValue=test2)]);
Actually, there were zero interactions with this mock.
There's probably something fundamental I don't understand about coroutines (or testing in general) and doing something wrong.
If I debug the tests I find that the failing test is not waiting for the inner runBlocking to complete. Actually the reason I have the inner runBlocking in the first place is to solve this exact problem and it seemed to work for individual tests.
Any ideas as to why this might be happening?
Test class
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class ViewModelTest {
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var mainThreadSurrogate: ExecutorCoroutineDispatcher
#Mock
lateinit var repository: DataSource
#Mock
lateinit var observer: Observer<List<SomeObject>>
private lateinit var viewModel: SomeViewModel
#Before
fun setUp() {
mainThreadSurrogate = newSingleThreadContext("UI thread")
Dispatchers.setMain(mainThreadSurrogate)
viewModel = SomeViewModel(repository)
}
#After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
#Test
fun `loadObjects1 should get objects1`() = runBlocking {
viewModel.someObjects1.observeForever(observer)
val expectedResult = listOf(SomeObject("test1"))
`when`(repository.getSomeObjects1Async())
.thenReturn(expectedResult)
runBlocking {
viewModel.loadSomeobjects1()
}
verify(observer).onChanged(listOf(SomeObject("test1")))
}
#Test
fun `loadObjects2 should get objects2`() = runBlocking {
viewModel.someObjects2.observeForever(observer)
val expectedResult = listOf(SomeObject("test2"))
`when`(repository.getSomeObjects2Async())
.thenReturn(expectedResult)
runBlocking {
viewModel.loadSomeObjects2()
}
verify(observer).onChanged(listOf(SomeObject("test2")))
}
}
ViewModel
class SomeViewModel constructor(private val repository: DataSource) :
ViewModel(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var objects1Job: Job? = null
private var objects2Job: Job? = null
val someObjects1 = MutableLiveData<List<SomeObject>>()
val someObjects2 = MutableLiveData<List<SomeObject>>()
fun loadSomeObjects1() {
objects1Job = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
}
fun loadSomeObjects2() {
objects2Job = launch {
val objects2Result = repository.getSomeObjects2Async()
objects2.value = objects2Result
}
}
override fun onCleared() {
super.onCleared()
objects1Job?.cancel()
objects2Job?.cancel()
}
}
Repository
class Repository(private val remoteDataSource: DataSource) : DataSource {
override suspend fun getSomeObjects1Async(): List<SomeObject> {
return remoteDataSource.getSomeObjects1Async()
}
override suspend fun getSomeObjects2Async(): List<SomeObject> {
return remoteDataSource.getSomeObjects2Async()
}
}
When you use launch, you're creating a coroutine which will execute asynchronously. Using runBlocking does nothing to affect that.
Your tests are failing because the stuff inside your launches will happen, but hasn't happened yet.
The simplest way to ensure that your launches have executed before doing any assertions is to call .join() on them.
fun someLaunch() : Job = launch {
foo()
}
#Test
fun `test some launch`() = runBlocking {
someLaunch().join()
verify { foo() }
}
Instead of saving off individual Jobs in your ViewModel, in onCleared() you can implement your CoroutineScope like so:
class MyViewModel : ViewModel(), CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext : CoroutineContext
get() = job + Dispatchers.Main
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
All launches which happen within a CoroutineScope become children of that CoroutineScope, so if you cancel that job (which is effectively cancelling the CoroutineScope), then you cancel all coroutines executing within that scope.
So, once you've cleaned up your CoroutineScope implementation, you can make your ViewModel functions just return Jobs:
fun loadSomeObjects1() = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
and now you can test them easily with a .join():
#Test
fun `loadObjects1 should get objects1`() = runBlocking {
viewModel.someObjects1.observeForever(observer)
val expectedResult = listOf(SomeObject("test1"))
`when`(repository.getSomeObjects1Async())
.thenReturn(expectedResult)
viewModel.loadSomeobjects1().join()
verify(observer).onChanged(listOf(SomeObject("test1")))
}
I also noticed that you're using Dispatchers.Main for your ViewModel. This means that you will by default execute all coroutines on the main thread. You should think about whether that's really something that you want to do. After all, very few non-UI things in Android need to be done on the main thread, and your ViewModel shouldn't be manipulating the UI directly.
Is there any way to wait for a suspending function that is running in a scope, like what runBlocking does for its running suspending functions?
For example,
class CoroutineTestCase : CoroutineScope {
val job = Job()
var value = 1
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Unconfined
fun testFunction() {
async {
delay(2000)
value = 2
}
}
}
#Test
fun testCoroutine() = runBlocking {
val coroutineTestCase = CoroutineTestCase()
coroutineTestCase.testFunction()
assertEquals(2, coroutineTestCase.value)
}
The above test fails with value being 1 and not changed (since the launch was not being waited to finish). If the testFunction had been a suspending function and I ran it with runBlocking inside my unit test, everything would have worked.
I've tried with other custom dispatchers (like the one below) that can blockingly run their tasks, but no luck
class TestUiContext : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
Okay so I figured out what is happening. The launch is not awaited because its returned value is never used.
In the example above, the testFunction should return the returned value of launch, which is a Deffered object that can be awaited/joined. So to actually wait for its completion, the code has to be changed as below:
class CoroutineTestCase : CoroutineScope {
val job = Job()
var value = 1
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Unconfined
fun testFunction(): Deferred<Unit> {
return async {
delay(20000)
value = 2
}
}
}
#Test
fun testCoroutine() = runBlocking {
val coroutineTestCase = CoroutineTestCase()
coroutineTestCase.testFunction().await()
assertEquals(2, coroutineTestCase.value)
}
Currently the only problem is that, in this case, it actually delays 20 seconds (with the unconfined dispatcher).
When I add a coroutine delay() in my view model, the remaining part of the code will not be executed.
This is my demo code:
class SimpleViewModel : ViewModel(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Unconfined
var data = 0
fun doSomething() {
launch {
delay(1000)
data = 1
}
}
}
class ScopedViewModelTest {
#Test
fun coroutineDelay() {
// Arrange
val viewModel = SimpleViewModel()
// ActTes
viewModel.doSomething()
// Assert
Assert.assertEquals(1, viewModel.data)
}
}
I got the assertion result:
java.lang.AssertionError:
Expected :1
Actual :0
Any idea how to fix this?
You start a coroutine which suspends for 1 second before setting data to 1. Your test just invokes doSomething but does not wait until data is actually being set. If you add another, longer delay, to the test it will, work:
#Test
fun coroutineDelay() = runBlocking {
...
viewModel.doSomething()
delay(1100)
...
}
You can also make the coroutine return a Deferred which you can wait on:
fun doSomething(): Deferred<Unit> {
return async {
delay(1000)
data = 1
}
}
With await there's no need to delay your code anymore:
val model = SimpleViewModel()
model.doSomething().await()
The first issue in your code is that SimpleViewModel.coroutineContext has no Job associated with it. The whole point of making your view model a CoroutineScope is the ability to centralize the cancelling of all coroutines it starts. So add the job as follows (note the absence of a custom getter):
class SimpleViewModel : ViewModel(), CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Unconfined
var data = 0
fun doSomething() {
launch {
delay(1000)
data = 1
}
}
}
Now your test code can ensure it proceeds to the assertions only after all the jobs your view model launched are done:
class ScopedViewModelTest {
#Test
fun coroutineDelay() {
// Arrange
val viewModel = SimpleViewModel()
// ActTes
viewModel.doSomething()
// Assert
runBlocking {
viewModel.coroutineContext[Job]!!.children.forEach { it.join() }
}
Assert.assertEquals(1, viewModel.data)
}
}