Polling repository with coroutine channels - kotlin

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
}
}

Related

How to test a stateflow that updates (emits) infinitely?

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()
}
}

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 test project reactor code that does not return a subscription

i'm trying to create a component that stream data from remote service continuously. The component starts and stops according to spring container lifecycle. I'm not sure how to test this component as the subscription is done inside my component so i was wondering wether this is the correct way to implement this kind of component with webflux or not. Does anybody know any similar component in any framework from where i might take some ideas?
Regards
class StreamingTaskAdapter(
private val streamEventsUseCase: StreamEventsUseCase,
private val subscriptionProperties: subscriptionProperties,
) : SmartLifecycle, DisposableBean, BeanNameAware {
private lateinit var disposable: Disposable
private var running: Boolean = false
private var beanName: String = "StreamingTaskAdapter"
private val logger = KotlinLogging.logger {}
override fun start() {
logger.info { "Starting container with name $beanName" }
running = true
doStart()
}
private fun doStart() {
disposable = Mono.just(
CreateSubscriptionCommand(
subscriptionProperties.events,
subscriptionProperties.owningApplication
)
)
.flatMap(streamEventsUseCase::createSubscription)
.flatMap { subscription ->
Mono.just(subscription)
.map(::ConsumeSubscriptionCommand)
.flatMap(streamEventsUseCase::consumeSubscription)
}
.repeat()
.retryWhen(Retry.backoff(MAX_ATTEMPTS, Duration.ofSeconds(2)).jitter(0.75))
.doOnSubscribe { logger.info { "Started event streaming" } }
.doOnTerminate { logger.info { "Stopped event streaming" } }
.subscribe()
}
override fun stop() {
logger.info("Stopping container with name $beanName")
doStop()
}
override fun isRunning(): Boolean = running
private fun doStop() {
running = false
disposable.dispose()
}
override fun destroy() {
logger.info("Destroying container with name $beanName")
doStop()
}
override fun setBeanName(name: String) {
this.beanName = name
}
companion object {
const val MAX_ATTEMPTS: Long = 3
}
}

My first observer called correctly but another was not called after inserted data to room database in kotlin android

In the application, I am fetching data from the web and from the observer change method, Insert that data to local db. that's fine. but after inserted to db, My second observer not called so my UI will not update.
ManActivity.class
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var adapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout.activity_main)
setupViewModel()
setupUI()
setupObservers()
setupObservers2()
}
private fun setupViewModel() {
viewModel = ViewModelProviders.of(
this,
ViewModelFactory(ApiHelper(RetrofitBuilder.apiService))
).get(MainViewModel::class.java)
}
private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MainAdapter(arrayListOf())
recyclerView.addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
recyclerView.adapter = adapter
}
private fun setupObservers() {
viewModel.getUsers().observe(this, Observer {
//viewModel.getUserFromWeb()
it?.let { resource ->
when (resource.status) {
SUCCESS -> {
Log.d("MYLOG","MyAPIChange success")
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
resource.data?.let {
users -> viewModel.setUserListToDB(this,users)
//sleep(1000)
}
}
ERROR -> {
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
Log.d("MYLOG","MyAPIChange error")
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
LOADING -> {
Log.d("MYLOG","MyAPIChange loading")
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
})
}
private fun setupObservers2() {
viewModel.getUserFromDB(this).observe(this, Observer {
users -> retrieveList(users)
Log.d("MYLOG","..MyDBChange")
})
}
private fun retrieveList(users: List<User>) {
adapter.apply {
addUsers(users)
notifyDataSetChanged()
}
}
}
MyViewModel.class
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
//lateinit var tempUser : MutableLiveData<List<User>>
fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
//emit(mainRepository.getUsers()) //direct call
}
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
fun setUserListToDB(context: Context, userList: List<User>) {
/*GlobalScope.launch {
mainRepository.setUserList(context, userList)
}*/
CoroutineScope(Dispatchers.IO).launch {
mainRepository.setUserList(context, userList)
}
}
}
MyRepository.class
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getUsers() = apiHelper.getUsers() // get from web
companion object {
var myDatabase: MyDatabase? = null
lateinit var userList: List<User>
fun initializeDB(context: Context): MyDatabase {
return MyDatabase.getDataseClient(context)
}
/*fun insertData(context: Context, username: String, password: String) {
myDatabase = initializeDB(context)
CoroutineScope(Dispatchers.IO).launch {
val loginDetails = User(username, password)
myDatabase!!.myDao().InsertData(loginDetails)
}
}*/
}
//fun getUserList(context: Context, username: String) : LiveData<LoginTableModel>? {
suspend fun getUserList(context: Context) : List<User> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.size.toString())
return userList
}
fun setUserList(context: Context,userList: List<User>){
myDatabase = initializeDB(context)
/*CoroutineScope(Dispatchers.IO).launch {
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
}*/
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
/*val thread = Thread {
myDatabase!!.myDao().InsertAllUser(userList)
}
Log.d("MYLOG","MyDBInserted")
thread.start()*/
}
}
DAO class
#Dao
interface DAOAccess {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun InsertAllUser(userList: List<User>)
// #Query("SELECT * FROM User WHERE Username =:username")
// fun getLoginDetails(username: String?) : LiveData<LoginTableModel>
#Query("SELECT * FROM User")
suspend fun getUserList() : List<User>
}
RetrofitBuilder
object RetrofitBuilder {
private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
Please can you know that what I am doing wrong here and why the second observer was not called after insert to db
Actually It was called when the screen launched but that time data not inserted so list size was 0 and after insert data this method will not call again. But Once I close app and again start then data will display bcoz at launch time, this method call and data got
I don't have enough reputation to commet, therefore I just bring a suggestion in this answer:
Suggestion/Solution
Room supports LiveData out of the box. So in your DAO you can change
suspend fun getUserList() : List<User>
to
suspend fun getUserList() : LiveData<List<User>>
Then in your repository adjust to
suspend fun getUserList(context: Context) : LiveData<List<User>> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.value.size.toString())
return userList
}
and in the ViewModel
fun getUserFromDB(context: Context) = mainRepository.getUserList(context))
With these adjustments I think it should work.
Explaination
You used the liveData couroutines builder here
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
As far as I understand this builder, it is meant to execute some asynchronous/suspend task and as soon as this task finishes the liveData you created will emit the result. That means that you only once receive the state of the user list an emidiately emit the list to the observer one single time and then this liveData is done. It does not observe changes to the list in the DB the whole time.
That is why it works perfectly for observing the API call (you want to wait until the call is finished and emit the response one single time), but not for observing the DB state(you want to observe the user list in the DB all the time and emit changes to the observer whenever the list is changed)

WorkManager observe retrofit progress

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)