How to emit data from an asycnhronous callback using Kotlin Flow? - android

I'm starting to learn Kotlin Flow and Coroutines but I do not know how to make the code below works. What am I doing wrong?
interface MessagesListener {
fun onNewMessageReceived(message: String)
}
fun messages(): Flow<String> = flow {
val messagesListener = object : MessagesListener {
override fun onNewMessageReceived(message: String) {
// The line below generates the error 'Suspension functions can be called only within coroutine body'
emit(message)
}
}
val messagesPublisher = MessagesPublisher(messagesListener)
messagesPublisher.connect()
}

I believe you should be able to use callbackFlow ....something like:
fun messages(): Flow<String> = callbackFlow {
val messagesListener = object : MessagesListener {
override fun onNewMessageReceived(message: String) {
trySend(message)
}
}
val messagesPublisher = MessagesPublisher(messagesListener)
messagesPublisher.connect()
}

What you are trying to achieve is not possible because emit is a suspend function.
However, you can use callbackFlow which is designed for such cases to convert Listeners/Callbacks into Coroutine's Flows.
fun messages() = callbackFlow<String> {
val messagesListener = object : MessagesListener {
override fun onNewMessageReceived(message: String) {
trySend(message)
}
}
val messagesPublisher = MessagesPublisher(messagesListener)
messagesPublisher.connect()
}

Related

Test MutableSharedFlow with runTest

I’m having some trouble to test some state changes in my viewModel using MutableSharedFlow. For example, I have this class
class SampleViewModel : ViewModel() {
private val interactions = Channel<Any>()
private val state = MutableSharedFlow<Any>(
onBufferOverflow = BufferOverflow.DROP_LATEST,
extraBufferCapacity = 1
)
init {
viewModelScope.launch {
interactions.receiveAsFlow().collect { action ->
processAction(action)
}
}
}
fun flow() = state.asSharedFlow()
fun handleActions(action: Any) {
viewModelScope.launch {
interactions.send(action)
}
}
suspend fun processAction(action: Any) {
state.emit("Show loading state")
// process something...
state.emit("Show success state")
}
}
I'm trying to test it using this method (already using version 1.6.1):
#Test
fun testHandleActions() = runTest {
val emissions = mutableListOf<Any>()
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.flow().collect {
emissions.add(it)
}
}
viewModel.handleActions("")
assertTrue(emissions[0] is String)
assertTrue(emissions[1] is String)
job.cancel()
}
If I test the viewmodel.processAction() it works like a charm. But if I try to test handleActions() I only receive the first emission and then it throws an IndexOutOfBoundsException
If I use the deprecated runBlockingTest, it works
#Test
fun testHandleActionsWithRunBlockingTest() = runBlockingTest {
val emissions = mutableListOf<Any>()
val job = launch {
viewModel.flow().toList(emissions)
}
viewModel.handleActions("")
job.cancel()
assertTrue(emissions[0] is String)
assertTrue(emissions[1] is String)
}
Did I miss something using runTest block?

What is the substitute for runBlocking Coroutines in fragments and activities?

It is recommended to not use GlobalScope and runBlocking.
I have implemented changes in order to this topic:
End flow/coroutines task before go further null issue
However it doesn't work well as previously with runBlocking. In brief icon doesn't change, data is not on time.
My case is to change icon depending on the boolean.
usecase with Flow
class GetNotificationListItemDetailsUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseFlowUseCase<Unit, List<NotificationItemsResponse.NotificationItemData>>() {
override fun create(params: Unit): Flow<List<NotificationItemsResponse.NotificationItemData>> {
return flow{
emit(notificationDao.readAllData())
}
}
}
viewmodel
val actualNotificationList: Flow<List<NotificationItemsResponse.NotificationItemData>> = getNotificationListItemDetailsUseCase.build(Unit)
fragment
private fun getActualNotificationList() : Boolean {
lifecycleScope.launch {
vm.actualNotificationList
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
notificationData.value = response
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
isNotificationNotRead = (notificationDataString.contains(stringToCheck))
}
}
return isNotificationNotRead
}
on method onViewCreated I have initToolbar to check if it's true and make action, with runBlokcing worked.
fun initToolbar{
if (onReceived) {
Log.d("onReceivedGoes", "GOES IF")
} else {
Log.d("onReceivedGoes", "GOES ELSE")
getActualNotificationList()
}
onReceived = false
val item = menu.findItem(R.id.action_notification_list)
when {
isNotificationNotRead && !isOutcomed -> {
item.setIcon(R.drawable.image_icon_change)
}
}
coroutine job before change, it worked well
val job = GlobalScope.launch {
vm.getNotificationListItemDetailsUseCase.build(Unit).collect {
notificationData.value = it
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
isNotificationNotRead = (notificationDataString.contains(stringToCheck))
}
}
runBlocking {
job.join()
}
}
Another question is I have the same thing to do in MainActivity, but I do not use there a flow just suspend function.
UseCase
class UpdateNotificationListItemUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseUpdateBooleanUseCase<Int, Boolean, Boolean, Boolean, Unit>() {
override suspend fun create(itemId: Int, isRead: Boolean, isArchived: Boolean, isAccepted: Boolean){
notificationDao.updateBooleans(itemId, isRead, isArchived, isAccepted)
}
}
MainActivity
val job = GlobalScope.launch { vm.getIdWithUpdate() }
runBlocking {
job.join()
}
MainViewmodel
suspend fun getIdWithUpdate() {
var id = ""
id = notificationAppSessionStorage.getString(
notificationAppSessionStorage.getIncomingKeyValueStorage(),
""
)
if (id != "") {
updateNotificationListItemUseCase.build(id.toInt(), true, false, false)
}
}
}
EDIT1:
collect in fragments works perfectly, thanks
What about MainActivity and using this usecase with suspend fun without flow.
I have read documentation https://developer.android.com/kotlin/coroutines/coroutines-best-practices
val IODispatcher: CoroutineDispatcher = Dispatchers.IO
val externalScope: CoroutineScope = CoroutineScope(IODispatcher)
suspend {
externalScope.launch(IODispatcher) {
vm.getIdWithUpdate()
}.join()
}
Second option, but here I do not wait until job is done
suspend {
withContext(Dispatchers.IO) {
vm.getIdWithUpdate()
}
}
What do you think about it?
You can try to update the icon in the collect block:
private fun getActualNotificationList() = lifecycleScope.launch {
vm.actualNotificationList
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
notificationData.value = response
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
val isNotificationNotRead = (notificationDataString.contains(stringToCheck))
val item = menu.findItem(R.id.action_notification_list)
when {
isNotificationNotRead && !isOutcomed -> {
item.setIcon(R.drawable.image_icon_change)
}
}
}
}
Using runBlocking you are blocking the Main Thread, which may cause an ANR.

suspendCancellableCoroutine returns CompletedWithCancellation instead of the actual type

I ran into a weird issue that manifested itself when I updated the kotlinx-coroutines-core dependency from 1.3.2 to 1.3.3. However, the self-contained example below reproduces the issue with 1.3.2 as well.
I have an extension method for a callback-based operation queue. This extension method uses suspendCancellableCoroutine to wrap the callback usage and to convert it to a suspend function. Now, it all works otherwise, but the resulting object that is returned from the suspending function is not of type T, but CompletedWithCancellation<T>, which is a private class of the coroutine library.
The weird thing is, if I call c.resume("Foobar" as T, {}) inside the suspendCancellableCoroutine, it works just fine. When using the callback routine, the value is a String before passing to to c.resume(), but it gets wrapped in a CompletedWithCancellation object.
Here's the code that reproduces the issue:
#ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
#SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.plant(Timber.DebugTree())
setContentView(R.layout.activity_main)
val vm = ViewModelProviders.of(this)
.get(MainViewModel::class.java)
vm.liveData.observe(this, Observer {
findViewById<TextView>(R.id.mainText).text = "Got result: $it"
})
vm.getFoo()
}
}
#ExperimentalCoroutinesApi
class MainViewModel : ViewModel() {
private val manager = OperationManager()
val liveData = MutableLiveData<String>()
fun getFoo() {
viewModelScope.launch {
val op = Operation(manager, "Foobar")
val rawResult = op.get<Any>()
Timber.d("Raw result: $rawResult")
val op2 = Operation(manager, "Foobar")
val result = op2.get<String>()
Timber.d("Casted result: $result")
liveData.postValue(result)
}
}
}
class OperationManager {
private val operationQueue = ConcurrentLinkedQueue<Operation>()
private val handler = Handler(Looper.getMainLooper())
private val operationRunnable = Runnable { startOperations() }
private fun startOperations() {
val iter = operationQueue.iterator()
while (iter.hasNext()) {
val operation = iter.next()
operationQueue.remove(operation)
Timber.d("Executing operation $operation")
operation.onSuccess(operation.response)
}
}
fun run(operation: Operation) {
addToQueue(operation)
startDelayed()
}
private fun addToQueue(operation: Operation) {
operationQueue.add(operation)
}
private fun startDelayed() {
handler.removeCallbacks(operationRunnable)
handler.post(operationRunnable)
}
}
open class Operation(private val manager: OperationManager, val response: Any) {
private val listeners = mutableListOf<OperationListener>()
fun addListener(listener: OperationListener) {
listeners.add(listener)
}
fun execute() = manager.run(this)
fun onSuccess(data: Any) = listeners.forEach { it.onResult(data) }
}
#ExperimentalCoroutinesApi
suspend fun <T> Operation.get(): T = suspendCancellableCoroutine { c ->
#Suppress("UNCHECKED_CAST")
val callback = object : OperationListener {
override fun onResult(result: Any) {
Timber.d("get().onResult() -> $result")
c.resume(result as T, {})
}
}
addListener(callback)
execute()
}
interface OperationListener {
fun onResult(result: Any)
}
Do note that just before calling c.resume(), the type of result is String, as it should be. However, it's not String in getFoo() once the suspend function completes. What causes this?
The solution was in this:
c.resume(result as T)
Instead of:
c.resume(result as T, {})
It seems that the former handles the execution of resume() correctly after getResult() is called, whereas the latter only works if resume() is called before getResult().

How to return value from async coroutine scope such as ViewModelScope to your UI?

I'm trying to retrieve a single entry from the Database and successfully getting the value back in my View Model with the help of viewModelScope, but I want this value to be returned back to the calling function which resides in the fragment so it can be displayed on a TextView. I tried to return the value the conventional way but it didn't work. So, How Can I return this value from viewModelScope.launch to the calling function?
View Model
fun findbyID(id: Int) {
viewModelScope.launch {
val returnedrepo = repo.delete(id)
Log.e(TAG,returnedrepo.toString())
// how to return value from here to Fragment
}
}
Repository
suspend fun findbyID(id : Int):userentity{
val returneddao = Dao.findbyID(id)
Log.e(TAG,returneddao.toString())
return returneddao
}
LiveData can be used to get value from ViewModel to Fragment.
Make the function findbyID return LiveData and observe it in the fragment.
Function in ViewModel
fun findbyID(id: Int): LiveData</*your data type*/> {
val result = MutableLiveData</*your data type*/>()
viewModelScope.launch {
val returnedrepo = repo.delete(id)
result.postValue(returnedrepo)
}
return result.
}
Observer in Fragment
findbyId.observer(viewLifeCycleOwner, Observer { returnedrepo ->
/* logic to set the textview */
})
Thank you Nataraj KR for your Help!
Following is the code that worked for me.
View Model
class ViewModel(application: Application):AndroidViewModel(application) {
val TAG = "ViewModel"
val repo: theRepository
val alldata:LiveData<List<userentity>>
val returnedVal = MutableLiveData<userentity>()
init {
val getDao = UserRoomDatabase.getDatabase(application).userDao()
repo = theRepository(getDao)
alldata = repo.allUsers
}
fun findbyID(id: Int){
viewModelScope.launch {
returnedVal.value = repo.findbyID(id)
}
}
}
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val usermodel = ViewModelProvider(this).get(ViewModel::class.java)
usermodel.alldata.observe(this, Observer {
Log.e(TAG,usermodel.alldata.value.toString())
})
usermodel.returnedVal.observe(this, Observer {
tv1.text = usermodel.returnedVal.value.toString()
})
allData.setOnClickListener {
tv1.text = usermodel.alldata.value.toString()
}
findByID.setOnClickListener {
usermodel.findbyID(et2.text.toString().toInt())
}
}
Another way without using LiveData would be like this,
Similar to viewModelScope there is also a lifecycleScope available with lifecycle-aware components, which can be used from the UI layer. Following is the example,
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
findByID.setOnClickListener {
lifecycleScope.launch{
val res = usermodel.findbyID(et2.text.toString().toInt())
// use returned value to do anything.
}
}
}
ViewModel
//1st option
// make the function suspendable itself.use aync instead of launch and then
// use await to collect the returned value.
suspend fun findbyID(id: Int): userEntity {
val job = viewModelScope.async {
val returnedrepo = repo.delete(id)
Log.e(TAG,returnedrepo.toString())
return#async returnedrepo
}
return job.await()
}
//2nd option
// make the function suspendable itself. but switch the execution on IO
// thread.(since you are making a DB call)
suspend fun findbyID(id: Int): userEntity {
return withContext(Dispatchers.IO){
val returnedrepo = repo.delete(id)
Log.e(TAG,returnedrepo.toString())
return#withContext returnedrepo
}
}
Since LiveData is specific to Android Environment, Using Kotlin Flow becomes a better option in some places, which offers similar functionality.

How to Unit Test MVVM with Koin?

How to Unit Test MVVM with Koin ?
i've try to testing : link
But, i don't know why i get error("No Data in ViewModel") in ViewModelTest fun getLookUpLeagueList()
Repository
class LookUpLeagueRepository {
fun getLookUpLeague(idLeague: String): MutableLiveData<LookUpLeague> {
val lookUpLeague = MutableLiveData<LookUpLeague>()
APIService().getLookUpLeague(idLeague).enqueue(object : Callback<LookUpLeague> {
override fun onFailure(call: Call<LookUpLeague>, t: Throwable) {
d("TAG", "lookUpLeagueOnFailure ${t.localizedMessage}")
}
override fun onResponse(call: Call<LookUpLeague>, response: Response<LookUpLeague>) {
lookUpLeague.value = response.body()
}
})
return lookUpLeague
}
}
ViewModel
class LookUpLeagueViewModel(private val lookUpLeagueRepository: LookUpLeagueRepository) :
ViewModel() {
var lookUpLeagueList = MutableLiveData<LookUpLeague>()
fun getLookUpLeagueList(idLeague: String) {
lookUpLeagueList = lookUpLeagueRepository.getLookUpLeague(idLeague)
}
}
Module
val lookUpLeagueModule = module {
single { LookUpLeagueRepository() }
viewModel { LookUpLeagueViewModel(get()) }
}
ViewModel Test
class LookUpLeagueViewModelTest : KoinTest {
val lookUpLeagueViewModel: LookUpLeagueViewModel by inject()
val idLeague = "4328"
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
lateinit var observerData: Observer<LookUpLeague>
#Before
fun before() {
MockitoAnnotations.initMocks(this)
startKoin {
modules(lookUpLeagueModule)
}
}
#After
fun after() {
stopKoin()
}
#Test
fun getLookUpLeagueList() {
lookUpLeagueViewModel.lookUpLeagueList.observeForever(observerData)
lookUpLeagueViewModel.getLookUpLeagueList(idLeague)
val value = lookUpLeagueViewModel.lookUpLeagueList.value ?: error("No Data in ViewModel")
Mockito.verify(observerData).onChanged(value)
}
}
#Test
fun getLookUpLeagueList() {
lookUpLeagueViewModel.lookUpLeagueList.observeForever(observerData)
...
}
At this time lookUpLeagueList is an instance of MutableLiveData. Say this is MutableLiveData #1.
lookUpLeagueViewModel.getLookUpLeagueList(idLeague)
Executing the line above would call LookUpLeagueViewModel.getLookUpLeagueList function. Let's take a look inside it.
lookUpLeagueList = lookUpLeagueRepository.getLookUpLeague(idLeague)
A totally new MutableLiveData is created inside LookUpLeagueRepository. That is not the same one as the one observerData is observing. At this time lookUpLeagueViewModel.lookUpLeagueList refers to the new one, MutableLiveData #2 because you re-assigned it to var lookUpLeagueList.
val value = lookUpLeagueViewModel.lookUpLeagueList.value ?: error("No Data in ViewModel")
Therefore, you're actually querying against MutableLiveData #2 which is new, not observed, and empty. That's why value is null. Instead of declaring as var, you should make it val. Don't re-assign the variable, setValue or postValue to propagate the change.

Categories

Resources