I need to fetch some data from a REST API, everything is ok when I'm 4G or wifi connected but when I'm in airplane mode, the app crashes with : "E/AndroidRuntime: FATAL EXCEPTION: main"
Before that I have a log (not an error saying : "Skipped 1013 frames! The application may be doing too much work on its main thread.")
So I suppose fetching the API with no network crashes the app because it's running in the main thread. BUT I'm using coroutines and to me, I'm doing it right :
ViewModel
private val viewModelJob = SupervisorJob()
private val viewModelScope = CoroutineScope(viewModelJob + Dispatchers.Main)
init {
viewModelScope.launch {
videosRepository.refreshVideos()
}
}
Repository
suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
val playlist = Network.devbytes.getPlaylist().await()
//database.videoDao().insertAll(*playlist.asDatabaseModel())
}
}
Service
/**
* A retrofit service to fetch a devbyte playlist.
*/
interface DevbyteService {
#GET("devbytes.json")
fun getPlaylist(): Deferred<NetworkVideoContainer>
}
/**
* Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for
* full Kotlin compatibility.
*/
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
/**
* Main entry point for network access. Call like `Network.devbytes.getPlaylist()`
*/
object Network {
// Configure retrofit to parse JSON and use coroutines
private val retrofit = Retrofit.Builder()
.baseUrl("https://devbytes.udacity.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
val devbytes: DevbyteService = retrofit.create(DevbyteService::class.java)
}
So the complete chain is :
ViewModel -> coroutine with Dispatchers.Main
that calls Repository -> suspend function that launch a coroutine with Dispatchers.IO
that calls Service -> through object Network, I get a retrofit instance with a getPlaylist() that returns a Deferred, and the call to that method is in the repository with an await()
What am I doing wrong ?
Your API call is throwing an exception because there is no network connection (most likely UnknownHostException).
Wrap it in a try-catch and deal with the exception.
CoroutineExceptionHandler might be a solution. https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#coroutineexceptionhandler
When you turn on the airplane mode, the coroutine that do network calls will throw an exception.
In your case, you can do something like this.
val handler = CoroutineExceptionHandler { _, exception ->
//Handle your exception
}
init {
viewModelScope.launch(handler) {
videosRepository.refreshVideos()
}
}
Related
So here's the deal . This is the ApiInterface:
interface ApiInterface {
#GET("api/fetch-some-info")
suspend fun fetchSomeInfo(
#Query("some_query_param") queryParam: String,
): Call<Data>
}
Here is how I generate it
new Retrofit.Builder()
.client(mOkHttpClient)
.addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().build()))
.baseUrl(url)
.build().create(AdMediationV2.class);
here is Data class
#JsonClass(generateAdapter = true)
data class Data(
#Json(name = "data")
val info: String)
Now I am trying to call the function and receive data using KotlinExtensions for Retrofit, I am using implementation 'com.squareup.retrofit2:retrofit:2.9.0'
This is how I am trying to call this function. I want to keep a reference of the job so I might be able to cancel it later.
val job = CoroutineScope(Dispatchers.IO).async{
api.fetchSomeInfo("Param").await().let { data->
//handle response}
But I get this error
java.lang.IllegalArgumentException: Unable to create converter for retrofit2.Call<Data>
According to documentation I dont even need to add Call to interface, but unless I do I cannot call .await(). It is marked as red and this is what it says
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public suspend fun <T : Any> Call<TypeVariable(T)>.await(): TypeVariable(T) defined in retrofit2
public suspend fun <T : Any> Call<TypeVariable(T)?>.await(): TypeVariable(T)? defined in retrofit2
What am I doing wrong?
Note: The Api functions properly, when used in java code, call.enqueue() it works fine.
It also works just fine in this code :
var call : Deferred<Data>? = null
MainScope().launch {
call = async(Dispatchers.IO) {
api.fetchSomeInfo("Param")
}
call?.await().let {//handle reponse}
The function in your interface should return Data instead of Call<Data> since it's a suspend function. And since it's a suspend function, there will be no need for calling await() on anything. You can just call fetchSomeInfo() directly in a coroutine, without any concern about what dispatcher it is called from either.
interface ApiInterface {
#GET("api/fetch-some-info")
suspend fun fetchSomeInfo(
#Query("some_query_param") queryParam: String,
): Data
}
//...
val job = someCoroutineScope.launch {
try {
val data = api.fetchSomeInfo("Param")
// handle data
} catch (exception: Exception) {
// handle errors
}
}
Alternatively, you could remove the suspend keyword from your function so it does have to return a Call<Data> that you will then have to call await() on, but that's just adding convolution, so don't do that.
A couple notes about your coroutine... it is a code smell to create a coroutine scope just to launch a single coroutine and then immediately lose the reference. You should be launching from a persistent CoroutineScope that you presumably will be cancelling when you want it to go out of scope.
And when you call a Retrofit API (either by using a suspend function or calling await() on a Call object), you must wrap it in try/catch to handle any of the many possible errors that might occur.
So, I have a following function which does a basic request using Ktor client to get a list of users,
suspend fun createRequest(): List<User>? {
return withContext(Dispatchers.IO) {
try {
val client = HttpClient(CIO)
val response: HttpResponse = client.get("http://10.0.2.2:9999/users")
client.close()
val str = response.readText()
val itemType = object : TypeToken<List<User>>() {}.type
Gson().fromJson<List<User>>(str, itemType)
} catch (e: Exception) {
null
}
}
}
Now, I use this as following,
runBlocking {
val res = async {createRequest()}
val users = res.await()
Log.v("_APP_", users.toString())
}
But then I read runBlocking should be used in testing and debugging and is not recommended for production. then what do I use instead of runBlocking?
createRequest() is a suspend function and it must be called from a coroutine. There are a couple of coroutine builders:
runBlocking - It is not recommended for production because it blocks the current thread.
launch - launches a new coroutine concurrently with the rest of the code, which continues to work independently.
async - creates a coroutine and returns its future result as an implementation of Deferred.
In Android there are a couple of ways to launch a coroutine, please refer to these docs.
To launch a coroutine you should have an instance of CoroutineScope, it can be viewModelScope (from the docs), lifecycleScope (from the docs) or custom instance. The sample code will look similar to:
scope.launch {
val users = createRequest()
Log.v("_APP_", users?.toString())
}
My viewmodel calls the repository methods to fetch some data from room database and also from network.
class Repository #Inject constructor(
private val remoteDatasource: IRemoteSource,
private val localDatasource: ILocalSource,
private val subscriberScheduler: Scheduler,
private val observerScheduler: Scheduler
) : IRepository {
//this method fetches data from room
override fun getData(): Flowable<Boolean> {
return localDatasource.shouldFetchRemote().subscribeOn(subscriberScheduler)
.observeOn(observerScheduler)
}
// makes api call
override fun getRemoteData(): Flowable<Data> {
return remoteDatasource.getData().subscribeOn(subscriberScheduler)
.observeOn(observerScheduler)
}
subscriberScheduler is Schedulers.io() and observer scheduler is AndroidSchedulers.mainThread().
I get exception when I do query from room, saying that the opertion is in main thread.
Also when I get data from remote source, I check the thread, it is main thread, but this no exception like network call on main thread.
Here is my localsource class which uses room:
class Localsource constructor(private val dataDao: DataDao):ILocalSource {
override fun shouldFetchRemote(): Flowable<Boolean> {
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints
}
//exception thrown here
return Flowable.just(dataDao.isDataPresent() != 0)
}
Here is class for RemoteSource
#OpenForTesting
class Remotesource #Inject constructor():IRemoteSource{
override fun getData(): Flowable<Data> {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints but no exception is thrown like network call on main thread.
}
return service.getData().flatMap { Flowable.just(it.data) }
}
}
Your assumptions about what happens where are wrong. That is an issue.
Lets look at shouldFetchRemote() method.
//This part will always be on the main thread because it is run on it.
//Schedulers applied only for the created reactive
//stream(Flowable, Observable, Single etc.) but not for the rest of the code in the method.
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints
}
//exception thrown here
//Yes it is correct that exception is thrown in this line
//because you do reach for the database on the main thread here.
//It is because Flowable.just() creates stream out of the independent data
//that does not know anything about scheduler here.
// dataDao.isDataPresent() - is run on the main thread
//because it is not yet part of the reactive stream - only its result is!!!!
//That is crucial
return Flowable.just(dataDao.isDataPresent() != 0)
In order to include the function into a stream you need to take another approach. Room has an ability to return Flowables directly and store booleans. This way you can use it like this
In DAO
#Query(...)
Boolean isDataPresent(): Flowable<Boolean>
In your local source
override fun shouldFetchRemote(): Flowable<Boolean> = dataDao.isDataPresent()
This way it will work as expected because now the whole function is the part of reactive stream and will react to schedulers.
The same with remote sourse. Retrofit can return Observables or Flowables out of the box
interface Service{
#GET("data")
fun getData(): Flowable<Data>
}
// and the repo will be
val service = retrofit.create(Service::class.java)
override fun getData(): Flowable<Data> = service.getData()
This way everything will work as expected because now it is the part of a stream.
If you want to use plan data from Room or Retrofit - you can do it either. The only thing is Flowable.just() won't work.
For example for your local source you will need to do something like
//DAO
#Query(...)
Boolean isDataPresent(): Boolean
override fun shouldFetchRemote(): Flowable<Boolean> = Flowable.create<Boolean>(
{ emitter ->
emitter.onNext(dataDao.isDataPresent())
emitter.onComplete() //This is crucial because without onComplete the emitter won't emit anything
//There is also emitter.onError(throwable: Throwable) to handle errors
}, BackpressureStrategy.LATEST).toObservable() // there are different Backpressure Strategies
There are similar factories for Obserwable and other reactive stream.
And generally I would recommend you to read the documentation.
I am facing a weird issue while unit testing Coroutines. There are two tests on the class, when run individually, they both pass and when I run the complete test class, one fails with assertion error.
I am using MainCoroutineRule to use the TestCoroutineScope and relying on the latest Coroutine Testing Library
Here is the test :
#Test
fun testHomeIsLoadedWithShowsAndFavorites() {
runBlocking {
// Stubbing network and repository calls
whenever(tvMazeApi.getCurrentSchedule("US", currentDate))
.thenReturn(getFakeEpisodeList())
whenever(favoriteShowsRepository.allFavoriteShowIds())
.thenReturn(arrayListOf(1, 2))
}
mainCoroutineRule.runBlockingTest {
// call home viewmodel
homeViewModel.onScreenCreated()
// Check if loader is shown
assertThat(LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())).isEqualTo(Loading)
// Observe on home view state live data
val homeViewState = LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())
// Check for success data
assertThat(homeViewState is Success).isTrue()
val homeViewData = (homeViewState as Success).homeViewData
assertThat(homeViewData.episodes).isNotEmpty()
// compare the response with fake list
assertThat(homeViewData.episodes).hasSize(getFakeEpisodeList().size)
// compare the data and also order
assertThat(homeViewData.episodes).containsExactlyElementsIn(getFakeEpisodeViewDataList(true)).inOrder()
}
}
The other test is almost similar which tests for Shows without favorites. I am trying to test HomeViewModel method as:
homeViewStateLiveData.value = Loading
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
onError(exception)
}
viewModelScope.launch(coroutineExceptionHandler) {
// Get shows from network and favorites from room db on background thread
val favoriteShowsWithFavorites = withContext(Dispatchers.IO) {
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
getShowsWithFavorites(episodes, favoriteShowIds)
}
// Return the combined result on main thread
withContext(Dispatchers.Main) {
onSuccess(favoriteShowsWithFavorites)
}
}
}
I cannot find the actual cause of why the tests if run separately are passing and when the complete class is tested, one of them is failing. Pls help if I am missing something
Retrofit and Room that come with Coroutine support owner the suspend functions and move them off the UI thread by their own. Thus, they reduce the hassles of handling thread callbacks by the developers in a big way. Initially, I was moving the suspend calls of network and DB to IO via Dispatchers.IO explicitly. This was unnecessary and also leading unwanted context-switching leading to flaky test. Since the libraries, automatically do it, it was just about handling the data back on UI when available.
viewModelScope.launch(coroutineExceptionHandler) {
// Get favorite shows from db, suspend function in room will launch a new coroutine with IO dispatcher
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
// Get shows from network, suspend function in retrofit will launch a new coroutine with IO dispatcher
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
// Return the result on main thread via Dispatchers.Main
homeViewStateLiveData.value = Success(HomeViewData(getShowsWithFavorites(episodes, favoriteShowIds)))
}
I'm giving a try to Kotlin Coroutines inside an Android app, specifically I've imported Kotlin Coroutine Adapter for Retrofit.
Kotlin Coroutine Adapter changes Retrofit interface to return a Deferred<T> instead of Call<T>.
What I don't understand is how to launch this Deferred in a particular CoroutineContext that I want to. Consider following code:
class MyViewModel #Inject constructor(
private val foo: Foo,
#Named("ui") private val uiContext: CoroutineContext,
#Named("network") private val networkContext: CoroutineContext
) : ViewModel() {
fun performSomeJob(param: String) {
launch(uiContext) {
try {
val response = foo.bar(param).await()
myTextView.setText(response.name)
} catch (error: Throwable) {
Log.e(error)
}
}
}
Where foo.bar(param) returns Deferred<SomeModel>.
This code works, but I'm not sure on what CoroutineContext this foo.bar(param) is being executed (CommonPool??).
How to explicitly specify, that I want foo.bar(param) to be executed in a networkContext?
val response = async(networkContext) { foo.bar(param) }.await()
This code doesn't work, because response is evaluated to Deferred<SomeModel> instead of SomeModel (which I want to achieve).
The foo.bar() call doesn't start another coroutine, it just wraps the native Retrofit Call so that its state changes get propagated to Deferred. Retrofit manages its own threads to perform its operations and this works just as it would without the coroutine wrapper. If you have a specific concern, you can manage it by configuring Retrofit in the usual way.
The only thing that should matter to you is that your coroutine is executing in the UI context.