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)
}
})
}
}
Related
See this answer for "Unit test the new Kotlin coroutine StateFlow"
and this issue in Kotlin coroutines GitHub repo..
How can I test all emissions of this StateFlow (results variable)?
class Emitter(dispatcher: CoroutineContext) {
private val coroutineScope = CoroutineScope(dispatcher)
private val source = MutableStateFlow("INITIAL")
private val _results = MutableStateFlow<String?>(null)
val results = _results.asStateFlow()
init {
source
.emitLatestEvery(5.seconds)
.conflate()
.map(String::lowercase)
.onEach(_results::emit)
.launchIn(coroutineScope)
}
#OptIn(ExperimentalCoroutinesApi::class)
private fun <T> Flow<T>.emitLatestEvery(duration: Duration) =
transformLatest {
while (true) {
emit(it)
delay(duration)
}
}
fun changeSource(s: String) {
source.value = s
}
}
Here is my test. It does not finish even if I use emitter.results.take(3).toList(results):
class EmitterTest {
#OptIn(ExperimentalCoroutinesApi::class)
#Test fun `Sample test`() = runTest {
val dispatcher = UnconfinedTestDispatcher(testScheduler)
val emitter = Emitter(dispatcher)
val results = mutableListOf<String?>()
val job = launch(dispatcher) { emitter.results.toList(results) }
emitter.changeSource("a")
emitter.changeSource("aB")
emitter.changeSource("AbC")
Assertions.assertThat(results).isEqualTo(listOf(null, "initial", "a", "ab", "abc"))
job.cancel()
}
}
I am uploading the file using Kotlin workmanager. In CoroutineWorkmanager, I do file upload with a suspend upload function. I want to observe retrofit progress and show it on ui. I can see the retrofit progress state but I cannot observe it in workManager.
My request body class where I can see the retrofit progress state :
class ProgressRequestBody : RequestBody {
val mFile: File
val ignoreFirstNumberOfWriteToCalls : Int
constructor(mFile: File) : super(){
this.mFile = mFile
ignoreFirstNumberOfWriteToCalls = 0
}
constructor(mFile: File, ignoreFirstNumberOfWriteToCalls : Int) : super(){
this.mFile = mFile
this.ignoreFirstNumberOfWriteToCalls = ignoreFirstNumberOfWriteToCalls
}
var numWriteToCalls = 0
private val _shared = MutableStateFlow<Float>(0F)
val shared : StateFlow<Float> = _shared
override fun contentType(): MediaType? {
return "image/*".toMediaTypeOrNull()
}
#Throws(IOException::class)
override fun contentLength(): Long {
return mFile.length()
}
#Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
numWriteToCalls++
val fileLength = mFile.length()
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
val `in` = FileInputStream(mFile)
var uploaded: Long = 0
try {
var read: Int
var lastProgressPercentUpdate = 0.0f
read = `in`.read(buffer)
while (read != -1) {
uploaded += read.toLong()
sink.write(buffer, 0, read)
read = `in`.read(buffer)
if (numWriteToCalls > ignoreFirstNumberOfWriteToCalls ) {
val progress = (uploaded.toFloat() / fileLength.toFloat()) * 100f
if (progress - lastProgressPercentUpdate > 1 || progress == 100f) {
_shared.value = progress
Log.d("progress", "${shared.value}")
lastProgressPercentUpdate = progress
}
}
}
} finally {
`in`.close()
}
}
companion object {
private val DEFAULT_BUFFER_SIZE = 2048
}
}
The worker I uploaded the file to:
class UploadWorker #WorkerInject constructor(
private val repository: Repository,
#Assisted context: Context,
#Assisted params: WorkerParameters
): CoroutineWorker(context, params) {
private lateinit var result: UploadResult
#ObsoleteCoroutinesApi
#OptIn(ExperimentalCoroutinesApi::class)
#SuppressLint("RestrictedApi")
override suspend fun doWork(): Result {
return try{
val requestBody = ProgressRequestBody(File(fileUri!!.toUri().path))
val multipartBody = prepareBody(fileUri!!.toUri(), photoPart)
progressState(requestBody)
upload(multipartBody)
Result.Success()
}catch(e :Exception){
Result.failure()
}
}
private fun prepareBody( ): MultipartBody.Part {
return MultipartBody.Part.createFormData("photo", "photo", "image/*")
}
suspend fun upload(
multipartBody: MultipartBody.Part,
) {
repository.uploadPhotos(
multipartBody
).collect{ result ->
if (result is Result.Success) {
this.result = result.data
}
}
}
private suspend fun progressState(photoPart: ProgressRequestBody) {
coroutineScope {
launch {
photoPart.shared.collect{
setProgress(workDataOf(PROGRESS to it))
}
}
}
}
}
While this way I can't run the worker. I am getting the following error from the worker:
java.util.concurrent.CancellationException: Task was cancelled.
at androidx.work.impl.utils.futures.AbstractFuture.cancellationExceptionWithCause(AbstractFuture.java:1184)
at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:514)
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)
}
I'm playing with coroutine channels and I wanted to implemented a polling test project. The idea is that a viewmodel will listen for data from a repository that polls an endpoint repeatedly.
When I pass a coroutineScope to the repository, the polling works, however when I create a new coroutineSCope in the repository, I see the data being injected into the channel, but it's not received on the viewmodel.
So this works:
class PollingViewModel : ViewModel() {
val counter = MutableLiveData<String>().apply { value = "uninitialized" }
private val repository = Repository()
init {
viewModelScope.launch {
val channel = repository.poll(this /* scope */)
channel.consumeEach {
Log.d("foo", "Viewmodel received [$it]")
counter.postValue(it.toString())
}
}
}
}
class Repository {
private var startValue = 0
suspend fun poll(coroutineScope: CoroutineScope) =
coroutineScope.produce(capacity = Channel.CONFLATED) {
while (true) {
Log.d("foo", "Sending value [$startValue]")
send(startValue++)
delay(POLLING_PERIOD_MILLIS)
}
}
companion object {
private const val POLLING_PERIOD_MILLIS = 1000L
}
}
But this does not (viewmodel does not receive anything):
class PollingViewModel : ViewModel() {
val counter = MutableLiveData<String>().apply { value = "uninitialized" }
private val repository = Repository()
init {
viewModelScope.launch {
repository.poll().consumeEach {
Log.d("foo", "Viewmodel received [$it]")
counter.postValue(it.toString())
}
}
}
}
class Repository {
private var startValue = 0
suspend fun poll() = coroutineScope {
produce(capacity = Channel.CONFLATED) {
while (true) {
Log.d("foo", "Sending value [$startValue]")
send(startValue++)
delay(POLLING_PERIOD_MILLIS)
}
}
}
companion object {
private const val POLLING_PERIOD_MILLIS = 1000L
}
}
What is the issue with creating a coroutineScope at the repository level?
Looks like the solution is to create a new CoroutineContext in the repository:
class Repository {
private var startValue = 0
private val context: CoroutineContext by lazy(LazyThreadSafetyMode.NONE) {
Job() + Dispatchers.IO
}
suspend fun poll(): ReceiveChannel<Int> = coroutineScope {
produce(
context = context,
capacity = Channel.CONFLATED
) {
while (true) {
send(startValue++)
delay(POLLING_PERIOD_MILLIS)
}
}
}
companion object {
private const val POLLING_PERIOD_MILLIS = 1000L
}
}
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.