How to emit from a LiveData builder from a non-suspending callback function - kotlin

I'm new to LiveData and Kotlin Coroutines. I'm trying to use the Chromium Cronet library to make a request from my repository class to return a LiveData object. To return the liveData, I'm using the new LiveData builder (coroutines with LiveData). How would I emit the result from a successful Cronet request?
class CustomRepository #Inject constructor(private val context: Context, private val gson: Gson) : Repository {
private val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
override suspend fun getLiveData(): LiveData<List<MyItem>> = liveData(coroutineDispatcher) {
val executor = Executors.newSingleThreadExecutor()
val cronetEngineBuilder = CronetEngine.Builder(context)
val cronetEngine = cronetEngineBuilder.build()
val requestBuilder = cronetEngine.newUrlRequestBuilder(
"http://www.exampleApi.com/example",
CustomRequestCallback(gson),
executor
)
val request: UrlRequest = requestBuilder.build()
request.start()
}
class CustomRequestCallback(private val gson: Gson) : UrlRequest.Callback() {
override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, byteBuffer: ByteBuffer?) {
byteBuffer?.flip()
byteBuffer?.let {
val byteArray = ByteArray(it.remaining())
it.get(byteArray)
String(byteArray, Charset.forName("UTF-8"))
}.apply {
val myItems = gson.fromJson(this, MyItem::class.java)
// THIS IS WHAT I WANT TO EMIT
// emit(myItems) doesn't work since I'm not in a suspending function
}
byteBuffer?.clear()
request?.read(byteBuffer)
}
// other callbacks not shown
}
}

The solution involves wrapping the UrlRequest.Callback traditional callback structure in a suspendCoroutine builder.
I also captured my learning in a Medium article which discusses Cronet integration with LiveData and Kotlin Coroutines.
override suspend fun getLiveData(): LiveData<List<MyItem>> = liveData(coroutineDispatcher) {
lateinit var result: List<MyItem>
suspendCoroutine<List<MyItem>> { continuation ->
val requestBuilder = cronetEngine.newUrlRequestBuilder(
"http://www.exampleApi.com/example",
object : UrlRequest.Callback() {
// other callbacks not shown
override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, byteBuffer: ByteBuffer?) {
byteBuffer?.flip()
byteBuffer?.let {
val byteArray = ByteArray(it.remaining())
it.get(byteArray)
String(byteArray, Charset.forName("UTF-8"))
}.apply {
val myItems = gson.fromJson(this, MyItem::class.java)
result = myItems
continuation.resume(result)
}
byteBuffer?.clear()
request?.read(byteBuffer)
},
executor
)
val request: UrlRequest = requestBuilder.build()
request.start()
}
emit(result)
}

Related

RxJava Scheduler.Worker equivalent in Kotlin coroutines

I try to migrate below code which RxJava to Kotlin coroutines.
This uses uses RxJava Scheduler.Worker to do json parsin in own thread. Is there something similar in Kotlin Coroutines?
// RxJava
class MessagesRepository() {
private val messagesSubject = PublishSubject.create<Message>()
val messages = messagesSubject.toFlowable(BackpressureStrategy.BUFFER)
val scheduler = Schedulers.computation()
private fun setClientCallbacks() {
val worker = scheduler.createWorker()
val processMessage = { message: ApiMessage ->
worker.schedule {
val msg = moshiJsonAdapter.fromJson(message.toString())
messagesSubject.onNext(msg)
}
}
client.setCallback(object : Callback {
override fun messageArrived(topic: String, message: ApiMessage) {
processMessage(message)
}
})
}
}
// Coroutines
class MessagesRepository() {
private val _messages = MutableSharedFlow<Message>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val messages: SharedFlow<Message> = _messages.asSharedFlow()
private fun setClientCallbacks() {
client.setCallback(object : Callback {
override fun messageArrived(topic: String, message: ApiMessage) {
// How to move this json parsin to own thread
val msg = moshiJsonAdapter.fromJson(message.toString())
_messages.tryEmit(vehicle)
}
})
}
}

How to create an HttpResponse object with dummy values in ktor Kotlin?

I am using ktor for developing a microservice in Kotlin. For testing a method, I need to create a dummy HttpResponse (io.ktor.client.statement.HttpResponse to be specific) object with status = 200 and body = some json data.
Any idea how I can create it?
You can use mockk or a similar kind of library to mock an HttpResponse. Unfortunately, this is complicated because HttpRequest, HttpResponse, and HttpClient objects are tightly coupled with the HttpClientCall. Here is an example of how you can do that:
val call = mockk<HttpClientCall> {
every { client } returns mockk {}
coEvery { receive(io.ktor.util.reflect.typeInfo<String>()) } returns "body"
every { coroutineContext } returns EmptyCoroutineContext
every { attributes } returns Attributes()
every { request } returns object : HttpRequest {
override val call: HttpClientCall = this#mockk
override val attributes: Attributes = Attributes()
override val content: OutgoingContent = object : OutgoingContent.NoContent() {}
override val headers: Headers = Headers.Empty
override val method: HttpMethod = HttpMethod.Get
override val url: Url = Url("/")
}
every { response } returns object : HttpResponse() {
override val call: HttpClientCall = this#mockk
override val content: ByteReadChannel = ByteReadChannel("body")
override val coroutineContext: CoroutineContext = EmptyCoroutineContext
override val headers: Headers = Headers.Empty
override val requestTime: GMTDate = GMTDate.START
override val responseTime: GMTDate = GMTDate.START
override val status: HttpStatusCode = HttpStatusCode.OK
override val version: HttpProtocolVersion = HttpProtocolVersion.HTTP_1_1
}
}
val response = call.response
I did this with following. I only needed to pass a status code and description, so I didn't bother about other fields.
class CustomHttpResponse(
private val statusCode: Int,
private val description: String
) :
HttpResponse() {
#InternalAPI
override val content: ByteReadChannel
get() = ByteReadChannel("")
override val call: HttpClientCall
get() = HttpClientCall(HttpClient())
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
override val headers: Headers
get() = Headers.Empty
override val requestTime: GMTDate
get() = GMTDate()
override val responseTime: GMTDate
get() = GMTDate()
override val status: HttpStatusCode
get() = HttpStatusCode(statusCode, description)
override val version: HttpProtocolVersion
get() = HttpProtocolVersion(name = "HTTP", major = 1, minor = 1)}
With Ktor 2, it's best to use externalServices block instead of attempting to mock HttpResponse. That way you don't need to attempt and mock the internals of Ktor, and it's not complicated at all.
externalServices {
hosts("https://your-fake-host") {
routing {
get("/api/v1/something/{id}/") {
call.respondText(
"{}",
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
}
}
}
This need to be wrapped with testApplication

Listening to coroutine from view cant be done from the views init

I am trying to listen to my ViewModels MutableStateFlow from my FlutterSceneView. But I get the following error when trying to set the listener from the views init:
Suspend function 'listenToBackgroundColor' should be called only from a coroutine or another suspend function
class FlutterSceneView(context: Context, private val viewModel: FlutterSceneViewModelType): PlatformView {
private val context = context
private val sceneView = SceneView(context)
init {
listenToBackgroundColor() // Error here
}
private suspend fun listenToBackgroundColor() {
viewModel.colorFlow.collect {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}
}
}
My ViewModel:
interface FlutterSceneViewModelType {
var colorFlow: MutableStateFlow<String>
}
class FlutterSceneViewModel(private val database: Database): FlutterSceneViewModelType, ViewModel() {
override var colorFlow = MutableStateFlow<String>("#FFFFFF")
init {
listenToBackgroundColorFlow()
}
private fun listenToBackgroundColorFlow() {
database.backgroundColorFlow.watch {
colorFlow.value = it.hex
}
}
}
the .watch call is a helper I have added so that this can be exposed to iOS using Kotlin multi-platform, it looks as follows but I can use collect instead if necessary:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
I resolved this by using viewModel context:
private fun listenToBackgroundColor() {
viewModel.colorFlow.onEach {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}.launchIn(viewModel.viewModelScope)
}
I had to import the following into my ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
from:
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")

Test a view model with livedata, coroutines (Kotlin)

I've been trying to test my view model for several days without success.
This is my view model :
class AdvertViewModel : ViewModel() {
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Default
private val scope = CoroutineScope(coroutineContext)
private val repository : AdvertRepository = AdvertRepository(ApiFactory.Apifactory.advertService)
val advertContactLiveData = MutableLiveData<String>()
fun fetchRequestContact(requestContact: RequestContact) {
scope.launch {
val advertContact = repository.requestContact(requestContact)
advertContactLiveData.postValue(advertContact)
}
}
}
This is my repository :
class AdvertRepository (private val api : AdvertService) : BaseRepository() {
suspend fun requestContact(requestContact: RequestContact) : String? {
val advertResponse = safeApiCall(
call = {api.requestContact(requestContact).await()},
errorMessage = "Error Request Contact"
)
return advertResponse
}
}
This is my view model test :
#RunWith(JUnit4::class)
class AdvertViewModelTest {
private val goodContact = RequestContact(...)
private lateinit var advertViewModel: AdvertViewModel
private var observer: Observer<String> = mock()
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setUp() {
advertViewModel = AdvertViewModel()
advertViewModel.advertContactLiveData.observeForever(observer)
}
#Test
fun fetchRequestContact_goodResponse() {
advertViewModel.fetchRequestContact(goodContact)
val captor = ArgumentCaptor.forClass(String::class.java)
captor.run {
verify(observer, times(1)).onChanged(capture())
assertEquals("someValue", value)
}
}
}
The method mock() :
inline fun <reified T> mock(): T = Mockito.mock(T::class.java)
I got this error :
Wanted but not invoked: observer.onChanged();
-> at com.vizzit.AdvertViewModelTest.fetchRequestContact_goodResponse(AdvertViewModelTest.kt:52)
Actually, there were zero interactions with this mock.
I don't understand how to retrieve the result of my query.
You would need to write a OneTimeObserver to observe livedata from the ViewModel
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onChanged(t: T) {
handler(t)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
After that you can write an extension function:
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler)
observe(observer, observer)
}
Than you can check this ViewModel class class that I have from a project to check what's going on with your LiveData after you act (when) with invoking a method.
As for your error, it just says that the onChanged() method is not being called ever.

RxJava + Retrofit Unit Test Kotlin Always Failed

I tried to create Unit Test using Rxjava + Retrofit but it always give an error.
I have tried all tutorials and reference related of my questions. I did success when create an unit test of other method (other case), but failed in this case (Rx + retrofit).
Request Data Code:
fun getDetailEvent(idEvent: String?) {
view.showLoading()
apiService.getDetailEvent(idEvent)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
val compositeDisposable: CompositeDisposable? = null
compositeDisposable?.add(it)
}
.doFinally { view.hideLoading() }
.subscribe({
val listModel = it
if (listModel != null) {
view.onDetailEventLoaded(listModel)
} else {
view.onDetailEventLoadFailed("Empty or Error List")
}
},
{
val errorMessage = it.message
if (errorMessage != null) {
view.onDetailEventLoadFailed(errorMessage)
}
})
}
Unit Test Code :
class DetailNextMatchPresenterTest {
#Mock
private lateinit var view : DetailNextMatchView
#Mock
private lateinit var apiService: ApiService
private lateinit var presenter: DetailNextMatchPresenter
#Before
fun setup(){
MockitoAnnotations.initMocks(this)
presenter = DetailNextMatchPresenter(view, apiService)
}
#Test
fun getDetailEvent() {
val event : MutableList<EventModel> = mutableListOf()
val response = ResponseEventModel(event)
val idEvent = "44163"
`when`(apiService.getDetailEvent(idEvent)
.test()
.assertSubscribed()
.assertValue(response)
.assertComplete()
.assertNoErrors()
)
presenter.getDetailEvent(idEvent)
verify(view).showLoading()
verify(view).onDetailEventLoaded(response)
verify(view).hideLoading()
}
}
I appreciate all suggestion. Thanks
I believe that the issue is that you haven't forced your code to behave synchronously in the context of your test, so the Observable runs in parallel to your test. Try adding this in your setup method:
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } If you're using RxJava2. Try looking for a similar method if you're using RxJava 1.