I have been doing TDD in Kotlin for these past few weeks now in Android using MVP. Things have been going well.
I use Mockito to mock classes but I can't seem to get over on how to implement one of the tests I wanted to run.
The following are my tests:
Call api, receive list of data, then show list. loadAllPlacesTest()
Call api, receive empty data, then show list. loadEmptyPlacesTest()
Call api, some exception happen on the way, then show error message. loadExceptionPlacesTest()
I have tests for #1 and #2 successfully. The problem is with #3, I'm not sure how to approach the test in code.
RestApiInterface.kt
interface RestApiInterface {
#GET(RestApiManager.PLACES_URL)
fun getPlacesPagedObservable(
#Header("header_access_token") accessToken: String?,
#Query("page") page: Int?
): Observable<PlacesWrapper>
}
RestApiManager.kt
the manager class implementing the interface looks like this:
open class RestApiManager: RestApiInterface{
var api: RestApiInterface
internal set
internal var retrofit: Retrofit
init {
val logging = HttpLoggingInterceptor()
// set your desired log level
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = okhttp3.OkHttpClient().newBuilder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor())
.build()
retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//very important for RXJAVA and retrofit
.build()
api = retrofit.create(RestApiInterface::class.java)
}
override fun getPlacesPagedObservable(accessToken: String?, page: Int?): Observable<PlacesWrapper> {
//return throw Exception("sorry2")
return api.getPlacesPagedObservable(
accessToken,
page)
}
}
}
Here is my unit test:
class PlacesPresenterImplTest : AndroidTest(){
lateinit var presenter:PlacesPresenterImpl
lateinit var view:PlacesView
lateinit var apiManager:RestApiManager
//lateinit var apiManager:RestApiManager
val EXCEPTION_MESSAGE1 = "SORRY"
val MANY_PLACES = Arrays.asList(PlaceItem(), PlaceItem());
var EXCEPTION_PLACES = Arrays.asList(PlaceItem(), PlaceItem());
val manyPlacesWrapper = PlacesWrapper(MANY_PLACES)
var exceptionPlacesWrapper = PlacesWrapper(EXCEPTION_PLACES)
val emptyPlacesWrapper = PlacesWrapper(Collections.emptyList())
#After
fun clear(){
RxJavaPlugins.reset()
}
#Before
fun init(){
//MOCKS THE subscribeOn(Schedulers.io()) to use the same thread the test is being run on
//Schedulers.trampoline() runs the test in the same thread used by the test
RxJavaPlugins.setIoSchedulerHandler { t -> Schedulers.trampoline() }
view = Mockito.mock<PlacesView>(PlacesView::class.java)
apiManager = Mockito.mock(RestApiManager::class.java)
presenter = PlacesPresenterImpl(view,context(), Bundle(), Schedulers.trampoline())
presenter.apiManager = apiManager
//exceptionPlacesWrapper = throw Exception(EXCEPTION_MESSAGE1);
}
#Test
fun loadAllPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(manyPlacesWrapper))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.atLeastOnce())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}
#Test
fun loadEmptyPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(emptyPlacesWrapper))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.atLeastOnce())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}
#Test
fun loadExceptionPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenThrow(Exception(EXCEPTION_MESSAGE1))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.never())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(EXCEPTION_MESSAGE1)
}
}
PlacesPresenterImpl.kt
This is the presenter.
class PlacesPresenterImpl
constructor(var view: PlacesView, var context: Context, var savedInstanceState:Bundle?, var mainThread: Scheduler)
: BasePresenter(), BasePresenterInterface, PlacesPresenterInterface {
lateinit var apiManager:RestApiInterface
var placeListRequest: Disposable? = null
override fun __firstInit() {
apiManager = RestApiManager()
}
override fun __init(context: Context, savedInstanceState: Bundle, view: BaseView?) {
this.view = view as PlacesView
if (__isFirstTimeLoad())
__firstInit()
}
override fun __destroy() {
placeListRequest?.dispose()
}
override fun __populate() {
_callPlacesApi()
}
override fun _callPlacesApi() {
view.__showLoading()
apiManager.getPlacesPagedObservable("", 0)
.subscribeOn(Schedulers.io())
.observeOn(mainThread)
.subscribe (object : DisposableObserver<PlacesWrapper>() {
override fun onNext(placesWrapper: PlacesWrapper) {
placesWrapper?.let {
val size = placesWrapper.place?.size
view.__hideLoading()
view._showList()
System.out.println("Great I found " + size + " records of places.")
view.__showFullScreenMessage("Great I found " + size + " records of places.")
}
System.out.println("onNext()")
}
override fun onError(e: Throwable) {
System.out.println("onError()")
//e.printStackTrace()
view.__hideLoading()
if (ExceptionsUtil.isNoNetworkException(e)){
view.__showFullScreenMessage("So sad, can not connect to network to get place list.")
}else{
view.__showFullScreenMessage("Oops, something went wrong. ["+e.localizedMessage+"]")
}
this.dispose()
}
override fun onComplete() {
this.dispose()
//System.out.printf("onComplete()")
}
})
}
private fun _getEventCompletionObserver(): DisposableObserver<String> {
return object : DisposableObserver<String>() {
override fun onNext(taskType: String) {
//_log(String.format("onNext %s task", taskType))
}
override fun onError(e: Throwable) {
//_log(String.format("Dang a task timeout"))
//Timber.e(e, "Timeout Demo exception")
}
override fun onComplete() {
//_log(String.format("task was completed"))
}
}
}}
Problem/Questions for the loadExceptionPlacesTest()
I'm not sure why the code doesn't go to the Presenter's onError().
correct me if I'm wrong the following but this is what I think:
a - `apiManager.getPlacesPagedObservable("", 0)` observable itself throws an Exception that is why the `.subscribe()` can not happen/proceed and the methods of the observer won't get called,
b - it will only go to onError() when the operations inside the observable encounters an Exception like JSONException
For loadExceptionPlacesTest() I think the 1b above is the way to go to make the presenter's onError() get called and make the test pass. Is this correct? If it is how to do it on the test. If it is not can you guys point out what I am missing or doing wrong?
I'll leave this here for future reference and to be able to elaborate a bit more, even though I've answered in the comments.
What you're trying to accomplish is to put the stream in the onError flow. Unfortunately, by mocking it like this:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenThrow(Exception(EXCEPTION_MESSAGE1))
You're actually telling Mockito to setup your mock in a way that just calling apiManager.getPlacesPagedObservable(anystring, anystring) should thrown an exception.
It is indeed true that throwing an exception inside an Rx stream will cause the entire stream to stop and end up in the onError method. However, this is exactly the problem with the approach you're using. You're not inside the stream when the exception is thrown.
Instead what you want to do is tell Mockito that once you call apiManager.getPlacesPagedObservable(anystring, anystring) you want to return a stream that will end up in the onError. This can be easily achieved with Observable.error() like so:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenReturn(Observable.error(
Exception(EXCEPTION_MESSAGE1)))
(It might be possible that you need to add some type information in this part here Observable.error(), you might also need to use something else instead of an observable - single, completable, etc.)
The mocking above will tell Mockito to setup your mock to return an observable that will error as soon as it's subscribed to. This will in turn put your subscriber directly in the onError stream with the specified exception.
Below is an example of a Test that invoke a REST service through Repository from a ViewModel according to the MVVM pattern.
The REST service returns an Exception, here is the test case:
#RunWith(AndroidJUnit4::class)
class StargazersViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Subject under test
private lateinit var viewModel: MyViewModel
#Mock
private lateinit var repositoryMock: MyRepository
#Before
fun setup() {
MockitoAnnotations.openMocks(this)
val appContext = ApplicationProvider.getApplicationContext<Application>()
viewModel = MyViewModel(repositoryMock, appContext)
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
}
#Test
fun `invoke rest with failure`() {
whenever(
repositoryMock.loadDataSingle(Mockito.anyString(), Mockito.anyInt())
).thenAnswer {
Single.error<retrofit2.HttpException>(
retrofit2.HttpException(
Response.error<String>(
404,
"Response.error()".toResponseBody("text/plain; charset=utf-8".toMediaType())
)
)
)
}
}
}
Related
I have a call to google place autocomplete sdk on a Rxjava that brings me a list of AutoCompletePredictions and then I use that list to iterate and call the place details sdk of google with that I want to return a single list with all the places details but it doesnt triggers.
fun searchAutoComplete(word: String): Single<MutableList<SuggestedPlace>> {
if (placeClient == null) {
placeClient = this.context?.let { Places.createClient(it) }
}
return Observable.create<SuggestedPlace> { emiter ->
var request = GooglePlaceHelper.getPlacesSuggestions(word)
placeClient?.findAutocompletePredictions(request)
?.addOnSuccessListener { response: FindAutocompletePredictionsResponse ->
response.autocompletePredictions.forEach { place ->
var request = GooglePlaceHelper.getPlaceDetailRequest(place.placeId)
placeClient?.fetchPlace(request)
?.addOnSuccessListener { response: FetchPlaceResponse ->
val place = response.place
place?.let {
var suggestedPlace = SuggestedPlace(place.address!!, place.latLng?.latitude!!, place.latLng?.longitude!!)
emiter.onNext(suggestedPlace)
}
}
}
}
}.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()).toList()
}
Please provide a proper example next time. It is quite work intensive to mock your APIs.
import io.reactivex.rxjava3.core.Single
import org.junit.jupiter.api.Test
This example will wrap the GoogleApi into a reactive-API, which provides Single functions. In order to collect all results in a List, you could use Single.zip.
Note: You should not use MutableList with RxJava. Always use immutable data types, or you get into trouble.
class So65684080 {
#Test
fun so65684080() {
val googleClientStub = GoogleClientStub()
val reactiveClient = GoogleClientReactiveImpl(googleClientStub)
val searchApiImpl = SearchApiImpl(reactiveClient)
searchApiImpl.search("whatever")
.test()
.assertValue(listOf(SuggestedPlace("fetchPlace"), SuggestedPlace("fetchPlace")))
}
}
internal interface SearchApi {
fun search(word: String): Single<List<SuggestedPlace>>
}
internal class SearchApiImpl(private val client: GoogleClientReactive) : SearchApi {
override fun search(word: String): Single<List<SuggestedPlace>> {
return client.findAutocompletePredictions("whatever")
.flatMap { resp ->
val fetches = resp.values.map { r -> client.fetchPlace(r) }
Single.zip(fetches) { arr ->
arr.map {
val fetchPlaceResponse = it as FetchPlaceResponse
SuggestedPlace(fetchPlaceResponse.name)
}
.toList()
}
}
}
}
internal interface GoogleClient {
fun findAutocompletePredictions(request: String): Result<FindAutocompletePredictionsResponse>
fun fetchPlace(request: String): Result<FetchPlaceResponse>
}
internal interface GoogleClientReactive {
fun findAutocompletePredictions(request: String): Single<FindAutocompletePredictionsResponse>
fun fetchPlace(request: String): Single<FetchPlaceResponse>
}
internal class GoogleClientStub : GoogleClient {
override fun findAutocompletePredictions(request: String): Result<FindAutocompletePredictionsResponse> {
return ResultStub<FindAutocompletePredictionsResponse>(FindAutocompletePredictionsResponse(listOf("fetch1", "fetch2")))
}
override fun fetchPlace(request: String): Result<FetchPlaceResponse> {
return ResultStub<FetchPlaceResponse>(FetchPlaceResponse("fetchPlace"))
}
}
internal class GoogleClientReactiveImpl(private val client: GoogleClient) : GoogleClientReactive {
override fun findAutocompletePredictions(request: String): Single<FindAutocompletePredictionsResponse> {
return Single.create { emitter ->
val response: (FindAutocompletePredictionsResponse) -> Unit = {
emitter.onSuccess(it)
}
client.findAutocompletePredictions(request).addOnSuccessListener(response)
// TODO: set emitter.setCancellable {} for unsubscribing
}
}
override fun fetchPlace(request: String): Single<FetchPlaceResponse> {
return Single.create { emitter ->
val response: (FetchPlaceResponse) -> Unit = {
emitter.onSuccess(it)
}
client.fetchPlace(request).addOnSuccessListener(response)
// TODO: set emitter.setCancellable {} for unsubscribing
}
}
}
internal data class SuggestedPlace(val name: String)
internal data class FetchPlaceResponse(val name: String)
internal data class FindAutocompletePredictionsResponse(val values: List<String>)
internal interface Result<T> {
fun addOnSuccessListener(response: (r: T) -> Unit)
}
internal class ResultStub<T>(val value: T) : Result<T> {
override fun addOnSuccessListener(response: (r: T) -> Unit) {
response(value)
}
}
Note
I did not add observeOn and subscribeOn, because it makes testing a little more difficulty. Please add it by yourself, at the end of the Single form SearchApiImpl#search
I'm trying to write a unit test waiting for completion of a kotlin suspended function before checking results like this :
#Test
fun shouldSetupThingsProperly() {
val context = InstrumentationRegistry.getInstrumentation().context
runBlocking { MyObject.enable(context, false) }
Assert.assertTrue( /* whatever usefull */ true)
}
The suspending methods are as follow :
object MyObject {
#JvmStatic
suspend fun enable(context: Context, enable: Boolean) {
withContext(Dispatchers.IO) {
// ... do some work
wakeup(context)
}
}
private suspend fun wakeup(context: Context) {
withContext(Dispatchers.IO) {
try {
// setup things ...
} catch (ignore: Exception) {}
}
}
}
Test run ends with :
java.lang.VerifyError: Verifier rejected class MyObject: java.lang.Object MyObject.enable(android.content.Context, boolean, kotlin.coroutines.Continuation) failed to verify: java.lang.Object MyObject.enable(android.content.Context, boolean, kotlin.coroutines.Continuation): [0x16] register v7 has type Reference: android.content.Context but expected Precise Reference: MyObject (declaration of 'MyObject' appears in /data/app/test-_rphd0tDrOp0KM-Bz09NWA==/base.apk!classes2.dex)
at MyObject.enable(Unknown Source:0)
I'm not familiar with coroutine and I was wondering how to achieve waiting for completion of the enable suspended function inside the test properly or if error was due to some other mistake...
If it happend to coroutines- withContext on Android or Flutter, revert coroutines lib to 1.3.6 solved crash issue for me.
It seems that there is VerifyError bug in android coroutines lib version 1.3.7-1.3.8, and will be fixed after 1.4.0.
Details see links:
https://github.com/Kotlin/kotlinx.coroutines/issues/2049 https://github.com/Kotlin/kotlinx.coroutines/issues/2041
Testing coroutines is a trick, even after some experience.
If you can import, this will be very helpful: https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test
If you have this dependency testing coroutines becomes much more manageable.
First off, if you can have the dispatcher you are running this a variable or parameter that can be set or overridden it will help you increase your testability.
As far as writing the test you can do something like:
#Before
fun before() {
Dispatchers.setMain(mainThreadSurrogate)
}
#Test
fun shouldSetupThingsProperly() = runBlockingTest {
val context = InstrumentationRegistry.getInstrumentation().context
MyObject.enable(context, false, Dispatchers.Main)
Assert.assertTrue( /* whatever useful */ true)
}
Your object itself will have I would say more of the changes
object MyObject {
#JvmStatic
suspend fun enable(context: Context, enable: Boolean, dispatcher: CoroutineDispatcher = Dispatchers.IO) {
// If you need a return feel free to use withContext such as:
// val result = withContext(dispatcher) { /* Return Value */ Any() }
CoroutineScope(dispatcher).run {
// ... do some work
wakeup(context)
}
}
private suspend fun wakeup(context: Context) {
// Another coroutine scope is unnecessary here, it will inherit the parent scope automatically, so you can call
// async functions here
delay(200)
try {
// setup things ...
} catch (exc: Exception) {
// We had an issue
}
}
}
I am working in a small feature for a Kotlin project and I am trying to do an annotation.
I want to "encapsulate" a method inside a try catch.
Lets say I annotate a method like this
#LogException
fun foo(){
//do something
}
So I want to process the annotation to later do something like this:
try{
foo()
}catch(exception: Exception){
// do something
//log
}
Is it even possible?
I started with tutorial for Kotlin (that should not be so different than Java) https://medium.com/#elye.project/annotation-tutorial-for-dummies-in-kotlin-1da864acc442 But then I am using this
#AutoService(Processor::class)
To register my processor and I think is not initializing my annotation.
So far this is my code:
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.SOURCE)
#Documented
annotation class LogException(
val name: String,
val statusCode: Int = 500
)
and my processor:
#AutoService(Processor::class)
class MyProcessor : AbstractProcessor() {
val logger = LoggerFactory.getLogger(MyProcessor::class.java)
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment?): Boolean {
//do something
logger.info("info")
return false
}
override fun getSupportedAnnotationTypes(): MutableSet<String> {
logger.info("info")
return mutableSetOf(LogException::class.java.canonicalName)
}
}
At the moment I am not able to see any in my logs, or even if I change it for an exception.
I am using this annotation in a method.
#LogException
fun foo(){
//do something
}
Any idea what Im missing?
//do something and //log something is just example to dont paste all code
It's not possible to change the existing code using annotation processors. They can only create new files. If you want to change existing ones, write Kotlin compiler plugin.
If you only want to get rid of boilerplate catch blocks, use an inline function which wraps an original function with try-catch:
inline fun runLogging(code: () -> Unit) = try {
code()
} catch (e: Exception) {
yourLogger.log(e)
}
fun foo() = runLogging {
throw Exception() // will be logged
}
Or you can make runLogging generic function which handles only specified exceptions:
inline fun <reified E : Exception> runLogging(code: () -> Unit) = try {
code()
} catch (e: Exception) {
if (e is E) yourLogger.log(e)
else throw e
}
fun foo() = runLogging<FileNotFoundException> {
throw FileNotFoundException() // will be logged
}
fun bar() = runLogging<FileNotFoundException> {
throw Exception() // will be rethrown
}
I created a PeriodicWorkRequest from my SyncDatabaseWorker, which like below:
class SyncDatabaseWorker(ctx: Context, params: WorkerParameters) : RxWorker(ctx, params) {
private val dataManager: DataManager = App.getDataManager()
override fun createWork(): Single<Result> {
return Single.create { emitter ->
dataManager.loadStoresFromServer()
.subscribe(object : SingleObserver<List<Store>> {
override fun onSubscribe(d: Disposable) {
}
override fun onSuccess(storeList: List<Store>) {
if (!storeList.isEmpty()) {
emitter.onSuccess(Result.success())
} else {
emitter.onSuccess(Result.retry())
}
}
override fun onError(e: Throwable) {
emitter.onSuccess(Result.failure())
}
})
}
}
companion object {
fun prepareSyncDBWorker(): PeriodicWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val myWorkBuilder = PeriodicWorkRequest.Builder(SyncDatabaseWorker::class.java, 7, TimeUnit.DAYS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.DAYS) // Backoff retry after 1 day
.setConstraints(constraints)
return myWorkBuilder.build()
}
}
}
Then I write unit test base on Google's guide like this:
#RunWith(AndroidJUnit4::class)
class SyncDatabaseWorkerTest {
private lateinit var context: Context
#Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
#Test
#Throws(Exception::class)
fun testPeriodicWork_WithConstrains() {
// Create request
val request = SyncDatabaseWorker.prepareSyncDBWorker()
val workManager = WorkManager.getInstance(context)
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
// Enqueue and wait for result.
workManager.enqueue(request).result.get()
// Check work request is enqueued
var workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state, `is`(WorkInfo.State.ENQUEUED))
// Tells the testing framework the period delay & all constrains is met
testDriver!!.setPeriodDelayMet(request.id)
testDriver.setAllConstraintsMet(request.id)
// Check work request is running
workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state, `is`(WorkInfo.State.RUNNING))
}
}
It's always in state ENQUEUED, even when period delay & all constrains is met.
Expected: is <RUNNING>
but: was <ENQUEUED>
When I debugged the test & find out that the createWork(): Single<Result> method is also triggered, but why the state is not RUNNING?
May be I'm wrong about the approach, but the documents about unit testing WorkManager is very few now, and I don't know the right way to do it.
Since you're using a synchronous executor, you will never actually see your work in the RUNNING state - it should have already executed. I suspect your work is actually being marked for retry and therefore enters the ENQUEUED state again. You should be able to verify this by either setting breakpoints or looking at your logs.
Given you are executing a PeriodicWorkRequest with a SynchronousExecutor, you will never see the WorkRequest in RUNNING state. It will be done executing before you can assert that it was in RUNNING.
After you return a Result.success() or a Result.failure() in your doWork(), the WorkRequest goes back to ENQUEUED for the next period (given it's a periodic request).
Currently, I have some scenario like this where I have java interface callback which looks something like this.
Java Callback
interface Callback<T> {
void onComplete(T result)
void onException(HttpResponse response, Exception ex)
}
Suspending function for the above look like this
suspend inline fun <T> awaitCallback(crossinline block: (Callback<T>) -> Unit) : T =
suspendCancellableCoroutine { cont ->
block(object : Callback<T> {
override fun onComplete(result: T) = cont.resume(result)
override fun onException(e: Exception?) {
e?.let { cont.resumeWithException(it) }
}
})
}
My calling function looks like this
fun getMovies(callback: Callback<Movie>) {
launch(UI) {
awaitCallback<Movie> {
// I want to delegate exceptions here.
fetchMovies(it)
}
}
What I'm currently doing to catch exception is this
fun getMovies(callback: CallbackWrapper<Movie>) {
launch(UI) {
try{
val data = awaitCallback<Movie> {
// I want to delegate exceptions here.
fetchMovies(it)
}
callback.onComplete(data)
}catch(ex: Exception) {
callback.onFailure(ex)
}
}
}
// I have to make a wrapper kotlin callback interface for achieving the above
interface CallbackWrapper<T> {
fun onComplete(result: T)
fun onFailure(ex: Exception)
}
Questions
The above works but is there any better way to do this?? One of the main thing is I'm currently migrating this code from callback so I have ~20 api calls and I don't want to add try/catch everywhere to delegate the result along with the exception.
Also, I'm only able to get exception from my suspending function is there any way to get both HttpResponse as well as the exception. Or is it possible to use existing JAVA interface.
Is there any better way to delegate the result from getMovies without using callback??
Is there any better way to delegate the result from getMovies without using callback?
Let me start with some assumptions:
you're using some async HTTP client library. It has some methods to send requests, for example httpGet and httpPost. They take callbacks.
you have ~20 methods like fetchMovies that send HTTP requests.
I propose to create an extension suspend fun for each HTTP client method that sends a request. For example, this turns an async client.httpGet() into a suspending client.awaitGet():
suspend fun <T> HttpClient.awaitGet(url: String) =
suspendCancellableCoroutine<T> { cont ->
httpGet(url, object : HttpCallback<T> {
override fun onComplete(result: T) = cont.resume(result)
override fun onException(response: HttpResponse?, e: Exception?) {
e?.also {
cont.resumeWithException(it)
} ?: run {
cont.resumeWithException(HttpException(
"${response!!.statusCode()}: ${response.message()}"
))
}
}
})
}
Based on this you can write suspend fun fetchMovies() or any other:
suspend fun fetchMovies(): List<Movie> =
client.awaitGet("http://example.org/movies")
My reduced example is missing the parsing logic that turns the HTTP response into Movie objects, but I don't think this affects the approach.
I'm currently migrating this code from callback so I have ~20 api calls and I don't want to add try/catch everywhere to delegate the result along with the exception.
You don't need a try-catch around each individual call. Organize your code so you just let the exception propagate upwards to the caller and have a central place where you handle exceptions. If you can't do that, it means you've got a specific way to handle each exception; then the try-catch is the best and idiomatic option. It's what you would write if you had a plain blocking API. Especially note how trivial it is to wrap many HTTP calls in a single try-catch, something you can't replicate with callbacks.
I'm only able to get exception from my suspending function is there any way to get both HttpResponse as well as the exception.
This is probably not what you need. What exactly do you plan to do with the response, knowing that it's an error response? In the example above I wrote some standard logic that creates an exception from the response. If you have to, you can catch that exception and provide custom logic at the call site.
I am not so sure whether you really need that awaitCallback or not.
If you really have lots of Callback already in place and that's why you used it then your functions will probably already have everything in place that works correctly with the Callback, e.g. I expect some methods as follows:
fun fetchMovies(callback : Callback<List<Movie>>) {
try {
// get some values from db or from a service...
callback.onComplete(listOf(Movie(1), Movie(2)))
} catch (e : Exception) {
callback.onFailure(e)
}
}
If you do not have something like this in place, you may not even need awaitCallback at all. So if your fetchMovies function rather has a signature as follows:
fun fetchMovies() : List<Movie>
and in getMovies you pass your Callback, then all you need is probably a simple async, e.g.:
fun getMovies(callback: Callback<List<Movie>>) {
GlobalScope.launch { // NOTE: this is now a suspend-block, check the parameters for launch
val job = async { fetchMovies() }
try {
callback.onComplete(job.await())
} catch (e: Exception) {
callback.onException(e)
}
}
}
This sample can of course be changed to many similar variants, e.g. the following will also work:
fun getMovies(callback: Callback<List<Movie>>) {
GlobalScope.launch { // NOTE: this is now a suspend-block, check the parameters for launch
val job = async { fetchMovies() } // you could now also cancel/await, or whatever the job
job.join() // we just join now as a sample
job.getCompletionExceptionOrNull()?.also(callback::onFailure)
?: job.getCompleted().also(callback::onComplete)
}
}
You could also add something like job.invokeOnCompletion. If you just wanted to pass any exception to your callback in your current code, you could just have used callback.onException(RuntimeException()) at the place where you put your comment I want to delegate exceptions here..
(note that I am using Kotlin 1.3 which is a RC now...)